Synchronous vs Asynchronous JavaScript: Callbacks → Promises → Async/Await
The three eras of asynchronous JavaScript, in order, and why each one exists specifically to fix a real problem with the one before it — not just as syntax sugar.
Async JavaScript didn't arrive as one design — it evolved in three visible stages, and each stage exists to fix a specific, nameable problem with the one before it. Knowing the problem each one solved is what actually explains why `async`/`await` looks the way it does.
Stage 1: callbacks, and the problem they created
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getInvoice(orders[0].id, (invoice) => {
// three levels deep, and error handling at every level
});
});
});"Callback hell" isn't just an aesthetic complaint — nested callbacks make error handling genuinely hard (each level needs its own error-first check) and make sequencing dependent operations read inside-out from how you'd describe the steps out loud.
Stage 2: Promises, and the problem they fixed
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => getInvoice(orders[0].id))
.catch((err) => handleError(err)); // one handler for the whole chainA Promise represents a value that will exist eventually, in one of three states: pending, fulfilled, or rejected. `.then()` chains flatten the nesting into a sequence, and — the actual fix — one `.catch()` handles a rejection from anywhere earlier in the chain, instead of needing an error check at every level.
Stage 3: async/await, and the problem it fixed
async function getInvoiceForUser(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
return await getInvoice(orders[0].id);
} catch (err) {
handleError(err);
}
}This is the exact same Promise chain from stage 2 — `async`/`await` is syntax on top of Promises, not a replacement for them. What it fixes is readability: the code reads top-to-bottom like synchronous code, and error handling goes back to an ordinary `try`/`catch` instead of a chain method. Under the hood, every `await` is still suspending at a microtask boundary, exactly as described in the event-loop post earlier in this series.
The snippets above are the short version — the full, runnable code lives on GitHub.