Asynchronous JavaScript
Asynchronous JavaScript
JavaScript runs on a single thread: it can only do one thing at a time. Yet real applications constantly wait — for network responses, timers, file reads. Asynchronous code is how JavaScript starts a slow operation, keeps the page responsive, and comes back when the result is ready. Mastering this model is the biggest single step from beginner to working developer.
Step 1: Callbacks — the Original Pattern
function fetchData(callback) {
setTimeout(() => {
callback("Data loaded");
}, 1000);
}
fetchData((data) => {
console.log(data); // "Data loaded" after 1 second
});
console.log("This prints FIRST");
Why the last line prints first: setTimeout hands the timer to the browser and returns immediately. Your script finishes, and only later does the event loop pick up the timer's callback and run it. Nothing blocks. The weakness appears when step B needs step A's result, and C needs B's — callbacks nest inside callbacks until the code collapses into the infamous "pyramid of doom".
Step 2: Promises — a Value That Arrives Later
A Promise is an object representing a future result. It starts pending, then settles exactly once: fulfilled with a value, or rejected with an error.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
// or reject(new Error("It failed"));
}, 1000);
});
promise
.then(result => console.log(result)) // "Success!"
.catch(error => console.error(error)) // runs only on rejection
.finally(() => console.log("Done either way"));
Why chaining beats nesting: every then() returns a new promise resolving to whatever the callback returns, so steps line up vertically, and one catch() at the end handles a failure from any step above it.
Step 3: async/await — Promises with Readable Syntax
async function fetchUser() {
try {
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
return null;
}
}
Why it works: await pauses this function only — not the whole program — until the promise settles, then hands back the fulfilled value or throws the rejection, which is why plain try/catch works again. An async function always returns a promise, so callers can await fetchUser() in turn. Checking response.ok matters because fetch only rejects on network failure; an HTTP 404 or 500 still fulfills.
Step 4: Running Work in Parallel
Sequential awaits add up. Independent operations should run together:
// SLOW — second request waits for the first (about 600ms total)
const users = await fetch("/api/users").then(r => r.json());
const posts = await fetch("/api/posts").then(r => r.json());
// FAST — both start immediately (about 300ms total)
const [users2, posts2] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json())
]);
Why: the requests begin the moment fetch is called. In the fast version both are in flight before you await anything, so the total time is the slowest request, not the sum. Remember Promise.all is all-or-nothing — one rejection fails the whole group; use Promise.allSettled when partial results are useful.
Step 5: Timeouts with Promise.race
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout!")), 5000)
);
const data = await Promise.race([
fetch("/api/data"),
timeout
]);
race settles with whichever promise finishes first — the real request or the rejecting timer — turning an endless wait into a catchable error after five seconds.
Common Beginner Errors and Fixes
- Forgetting await:
const user = fetchUser()stores a Promise object, anduser.nameisundefined. Fix:const user = await fetchUser(). If you ever log a pending Promise object instead of data, this is the bug. - await inside a loop for independent items: processing 10 items serially takes 10x as long. Fix:
await Promise.all(items.map(process)). - No rejection handling: an unhandled rejection crashes modern Node.js and fires
unhandledrejectionin browsers. Fix: every chain ends incatch(), every await sits in (or propagates to) atry/catch. - Using await outside an async function: a
SyntaxErrorin most contexts. Fix: mark the functionasync(or use top-level await in ES modules). - Mixing styles:
await promise.then(...)works but confuses readers. Fix: pickawaitand stick with it inside a function.
Practice Exercise
Simulate a tiny weather dashboard using only setTimeout:
getTemperature() returning a promise that fulfills with 21 after 500ms.getHumidity() returning a promise that fulfills with 64 after 800ms.loadDashboard() that fetches both in parallel, logs "21°C, 64% humidity", and measures elapsed time with Date.now().getHumidity() reject randomly (Math.random() < 0.5) and handle it so the dashboard still prints the temperature with "humidity unavailable".function getTemperature() {
return new Promise(resolve => setTimeout(() => resolve(21), 500));
}
function getHumidity() {
return new Promise(resolve => setTimeout(() => resolve(64), 800));
}
async function loadDashboard() {
const start = Date.now();
const [temp, hum] = await Promise.all([getTemperature(), getHumidity()]);
console.log(${temp}°C, ${hum}% humidity in ${Date.now() - start}ms);
}
loadDashboard();
The bonus pushes you toward Promise.allSettled or a per-promise catch — both are patterns you will use constantly in real projects.