Skip to content
Sahil Durgia/ full-stack
1 min readJavaScript

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.

JavaScriptasyncpromises

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 chain

A 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.

Runnable example

The snippets above are the short version — the full, runnable code lives on GitHub.

View on GitHub ★
Keep reading
Next: JavaScript type coercion

Part 8 of the JavaScript fundamentals series.