Asynchronous JavaScript
JavaScript does one thing at a time — yet the page never freezes. This is how.
0 Week at a glance
- Async programming — start slow work now, handle the result later, never freeze the page.
- Event loop — how one single thread fakes doing many things at once.
- Three tools — callbacks (old) · Promises (ES6) · async/await (ES2017, built on Promises).
- Fetch API — real data from real servers, returned as a Promise.
Every example is live. Press Run to execute — or click into the code and edit it. Flip auto-run (top-right of each example) to re-run automatically as you type.
1 Synchronous vs. asynchronous
Synchronous — blocking
- Each line finishes completely before the next starts. Order is guaranteed.
- Cost: while it runs, the page is frozen — no clicks, no scroll, no animation.
console.log("Start");
for (let i = 0; i < 5; i++) {
console.log("Processing...");
}
console.log("End");
Asynchronous — event-driven
- Register a function to run later, when something happens (click, timer, data arrives).
- Registration is instant — the rest of the code keeps going.
<button id="btn">Click me</button>
<script>
const button = document.getElementById("btn");
button.addEventListener("click", function () {
alert("Button was clicked!"); // runs LATER — only when clicked
});
console.log("Page loaded"); // runs NOW
</script>
Why we need it
| Benefit | Meaning |
|---|---|
| Better UX | Page stays responsive while data loads. |
| Non-blocking I/O | Network / file work runs in the background. |
| Performance | Many slow operations in flight at once. |
| Scalability | Same model powers Node.js servers with thousands of connections. |
2 How JavaScript executes
- Call stack — where your functions run, one at a time.
- Web APIs — browser helpers (timers, network, events) working outside the JS thread.
- Callback queue — finished background work waits here.
- Event loop — moves queued callbacks onto the stack only when it's empty.
Watch it happen
console.log("Start");setTimeout(() => { console.log("Processing...");}, 2000);console.log("End");
📚 Call stack (JS thread)
🌐 Web APIs (browser)
⏳ Callback queue
🖥️ Console
Exam favourite: a callback never interrupts running code. Even setTimeout(fn, 0) waits for the current code to finish — the delay is a minimum, not a guarantee.
3 setTimeout & timers
setTimeout(callbackFunction, delay, param1, param2, ...);
// callbackFunction : code to run after the delay
// delay : wait time in milliseconds
// params : optional arguments for the callback
The most important example of the week
console.log("Before setTimeout");
const secondInMilliseconds = 1000;
setTimeout(() => {
console.log("A second has passed");
}, secondInMilliseconds);
console.log("After setTimeout");
- JS does not stop at setTimeout — the timer goes to the browser, the code goes on.
- This one behaviour explains everything else this week.
4 Callbacks — the original technique
Callback = a function passed as an argument to another function, to be called back later. Used for HTTP requests, timers, user events, file operations.
const doSomething = (cb) => {
console.log("Doing something...");
cb(); // "calling back" the function we were given
};
const nextStep = () => {
console.log("Callback called");
};
doSomething(nextStep); // pass the FUNCTION itself — no parentheses!
Classic mistake: doSomething(nextStep()) calls it now. doSomething(nextStep) passes it to call later.
Callbacks for async work
function fetchData(callback) {
console.log("Fetching data...");
setTimeout(() => {
const data = { name: "Abul Qasim", role: "Professor" };
callback(data); // deliver the result via the callback
}, 2000);
}
function processData(data) {
console.log("Data received:", data);
}
fetchData(processData); // pass processData as the callback
5 Callback hell
The real thing, with the error checks every level needs. Try to follow it — that's the point:
// Five fake "API" functions — each takes (input, callback(err, result))
function getUser(id, cb) { setTimeout(() => cb(null, { id: id, name: "Dinn" }), 500); }
function getPosts(userId, cb) { setTimeout(() => cb(null, [{ id: 7, title: "Post 1" }]), 500); }
function getComments(postId, cb){ setTimeout(() => cb(null, [{ id: 99, by: 3, text: "Nice!" }]), 500); }
function getAuthor(userId, cb) { setTimeout(() => cb(null, { id: userId, name: "Abul Qasim" }), 500); }
function sendThanks(user, cb) { setTimeout(() => cb(null, "thanked " + user.name), 500); }
// The task: user -> posts -> comments -> comment author -> send thanks
getUser(1, function (err, user) {
if (err) { console.log("could not load user"); return; }
console.log("1. got user:", user.name);
getPosts(user.id, function (err, posts) {
if (err) { console.log("could not load posts"); return; }
console.log("2. got " + posts.length + " post(s)");
getComments(posts[0].id, function (err, comments) {
if (err) { console.log("could not load comments"); return; }
console.log("3. got comment:", comments[0].text);
getAuthor(comments[0].by, function (err, author) {
if (err) { console.log("could not load author"); return; }
console.log("4. comment author:", author.name);
sendThanks(author, function (err, status) {
if (err) { console.log("could not send thanks"); return; }
console.log("5. done:", status);
// Homework: try inserting ONE new step in the middle 😈
});
});
});
});
});
- Reads diagonally — the logic drifts right instead of down.
- 5 separate error checks — no single place to catch failures.
- Fragile — inserting or reordering one step means re-nesting everything below it.
6 Promises
Promise = an object representing a future value. An IOU: "you'll get the value — or a reason why not."
The three states
Once settled, the buttons stop working — a promise settles only once.
Creating a promise
- You get two functions from JS: call resolve(value) on success, reject(reason) on failure.
const myPromise = new Promise((resolve, reject) => {
let success = true; // flip to false → rejection path
setTimeout(() => {
if (success) {
resolve("Promise resolved successfully!");
} else {
reject("Promise rejected! Something went wrong.");
}
}, 2000);
});
myPromise
.then((message) => console.log("SUCCESS:", message))
.catch((error) => console.log("FAILURE:", error));
Consuming with .then()
- .then(callback) = "when fulfilled, run this with the value".
- Registering is instant — the callback runs later.
- Shortcuts: Promise.resolve(v) / Promise.reject(r) create already-settled promises.
const setTimeoutPromise = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
};
console.log("Before setTimeoutPromise");
setTimeoutPromise(1000).then(() => console.log("one second later"));
console.log("After setTimeoutPromise");
Chaining
const fetchData = new Promise((resolve) => {
setTimeout(() => {
resolve(10);
}, 1000);
});
fetchData
.then((number) => {
console.log("First then:", number); // 10
return number * 2;
})
.then((newNumber) => {
console.log("Second then:", newNumber); // 20
return newNumber + 5;
})
.then((finalResult) => {
console.log("Final result:", finalResult); // 25
});
Escaping callback hell
function fetchUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetched user");
resolve({ id: userId, name: "Dinn" });
}, 1000);
});
}
function fetchPosts(user) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetched posts for " + user.name);
resolve(["Post 1", "Post 2"]);
}, 1000);
});
}
function fetchComments(posts) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetched comments for " + posts[0]);
resolve(["Comment 1", "Comment 2"]);
}, 1000);
});
}
// Flat. Readable. Top-to-bottom.
fetchUser(1)
.then(fetchPosts)
.then(fetchComments)
.then((comments) => {
console.log("Final data:", comments);
});
Errors: .then / .catch / .finally
| Method | Runs when… |
|---|---|
| .then(cb) | fulfilled — and continues the chain. |
| .catch(cb) | any earlier step rejects or throws — one catch guards the whole chain. |
| .finally(cb) | always — cleanup (e.g. hide the spinner). |
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) { // coin toss
resolve("fresh data");
} else {
reject(new Error("server timeout"));
}
}, 800);
});
}
getData()
.then((data) => console.log("Step 1 complete:", data))
.then(() => console.log("Step 2 complete"))
.catch((error) => console.log("Something went wrong:", error.message))
.finally(() => console.log("All done! (runs either way)"));
Callbacks vs. Promises
| Feature | Callbacks | Promises |
|---|---|---|
| Definition | Function passed in, run later | Object holding a future value |
| Syntax | Nested functions | .then() / .catch() |
| Readability | Callback hell | Flat chains |
| Error handling | Manual, every level | One .catch() |
| Chaining | Hard — nesting | Natural |
7 Async / await — the modern way
async functions return promises
async function f() {
return 1;
}
f().then(console.log); // 1async function f() {
return Promise.resolve(1);
}
f().then(console.log); // 1Rules of the road
- await only inside async functions (or module top level).
- Fulfilled → returns the value. Rejected → throws.
- Pauses the function, not the browser — everything else keeps running.
- Awaits run one after another — great for dependent steps.
function makeCoffee() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("☕ Coffee is ready!");
}, 2000); // 2-second brew
});
}
async function prepareBreakfast() {
console.log("Preparing pancakes...");
const coffee = await makeCoffee(); // pause HERE until resolved
console.log(coffee);
console.log("🥞 Breakfast is ready!");
}
prepareBreakfast();
console.log("(meanwhile, the rest of the program keeps running…)");
Same logic, two styles
function loadData() {
fetchUser(1)
.then(function (user) {
return fetchPosts(user.id);
})
.then(function (posts) {
return fetchComments(posts[0].id);
})
.then(function (comments) {
console.log(comments);
})
.catch(function (error) {
console.log(error);
});
}async function loadData() {
try {
let user = await fetchUser(1);
let posts = await fetchPosts(user.id);
let comments = await fetchComments(posts[0].id);
console.log(comments);
} catch (error) {
console.log(error);
}
}Rejections → try / catch
function riskyOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("the server is on fire 🔥")), 700);
});
}
async function main() {
try {
console.log("Attempting operation…");
const result = await riskyOperation(); // THROWS on rejection
console.log("Success:", result); // skipped
} catch (error) {
console.log("Caught it:", error.message);
}
}
main();
The golden rule: every await that can reject must be covered by try/catch — otherwise: Uncaught (in promise).
8 The Fetch API
| Member | Gives you |
|---|---|
| response.status | HTTP status code (200, 404, 500…) |
| response.ok | true for 200–299 — always check! |
| response.text() | Body as a string — returns a Promise |
| response.json() | Body parsed into a JS object — also a Promise |
Two awaits, not one: reading the body is a second async step — await fetch(url), then await response.json().
Fetch with .then() — live demo
- Loads anime.json — a real file next to this page.
fetch("anime.json")
.then((response) => response.json()) // parse body as JSON
.then((data) => {
console.log("Loaded", data.length, "anime");
console.log("First title:", data[0].title);
})
.catch((error) => console.error("Error:", error.message));
The robust version — check response.ok
const apiUrl = "https://jsonplaceholder.typicode.com/posts";
fetch(apiUrl)
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok: " + response.status);
}
return response.json();
})
.then((data) => {
console.log("Received", data.length, "posts");
console.log("First post title:", data[0].title);
})
.catch((error) => {
console.error("Problem with the fetch operation:", error.message);
});
Fetch with async/await — walkthrough
async function loadJson() {
const response = await fetch("anime.json");
const json = await response.json();
console.log("Got " + json.length + " anime:", json.map(a => a.title).join(", "));
}
loadJson();
console.log("after loadJson");
- loadJson() starts.
- First await: fetch starts → function pauses → engine exits and prints "after loadJson".
- Response arrives → function resumes — the await gives us the response.
- Second await: parsing → pauses again.
- json is now a real array → final log runs.
9 Choosing a technique
| Callbacks (old) | Promises (ES6 · 2015) | Async/await (ES2017) | |
|---|---|---|---|
| Style | Nested functions | .then().catch() chain | Looks synchronous |
| Errors | Manual, each level | One .catch() | try/catch |
| Dependent steps | Pyramid of doom | Flat chain | One line per step |
| Debugging | Hard | Medium | Easiest |
| Still used for | Event listeners, timers | Libraries, Promise.all | New app code |
10 Key takeaways
- JS is single-threaded — the event loop + Web APIs fake multitasking.
- setTimeout never blocks — later code runs first; the callback waits for an empty stack.
- Callback = function passed in, called later. Nesting them = callback hell.
- Promise = future value. pending → fulfilled / rejected, settles once.
- .then / .catch / .finally — chain by returning values.
- async returns a Promise; await pauses the function (not the browser) and throws on rejection → try/catch.
- fetch → Response envelope → check response.ok → response.json() (second await).
11 Self-check quiz
No grades, instant feedback. Can you get all six?
Q1. What does this print, in order?
console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");
Q2. Which are the three states of a Promise?
Q3. What does an async function always return?
Q4. While an async function is paused at an await, the browser is…
Q5. Why do we call response.json() after fetch()?
Q6. A promise rejects inside an async function with no try/catch. What happens?
12 Reading & practice
Zybooks — Chapter 8 "More JavaScript": sections 8.13 – 8.15. Do the participation activities — they mirror this week's lab.
- MDN — Asynchronous JavaScript
- MDN — Using Promises
- MDN — Using the Fetch API
- JSONPlaceholder — free fake API for practicing fetch.