JavaScriptAdvanced

Asynchronous JavaScript: Callbacks, Promises & async/await

From callback hell to elegant async/await: how JavaScript models operations that finish later, how promises chain and fail, and how to run work in parallel safely.

4 sections · ~30 min · 5-question quiz (pass ≥ 70%)

1Why Async? From Callbacks to Callback Hell

JavaScript runs on one thread — so slow operations (network, disk, timers) must not block it. The original answer was callbacks: "here's a function, call it when you're done."

loadUser(id, (err, user) => {
  if (err) return handle(err);
  loadOrders(user, (err, orders) => {
    if (err) return handle(err);
    loadDetails(orders[0], (err, details) => {
      if (err) return handle(err);
      render(details);          // welcome to the pyramid of doom
    });
  });
});

Problems: deep nesting, error handling repeated at every level, and no way to compose or combine operations. Promises were designed to fix exactly this.

2Promises: Values From the Future

A Promise is an object representing an eventual result. It's in exactly one of three states: pendingfulfilled (with a value) or rejected (with a reason). Once settled, it never changes.

fetchUser(id)
  .then(user => fetchOrders(user))   // return a promise → chain waits for it
  .then(orders => render(orders))
  .catch(err => showError(err))      // ONE handler catches any failure above
  .finally(() => hideSpinner());     // runs either way

Key mechanics:

  • .then returns a new promise, which is what makes flat chaining possible.
  • Returning a promise inside .then inserts it into the chain.
  • A rejection skips forward to the nearest .catch — like a thrown exception falling through to a try/catch.
  • .catch returns a normal promise too, so the chain can recover and continue.

3async/await: Synchronous-Looking Async

async/await is syntax over promises — the same machinery, dramatically more readable:

async function showOrders(id) {
  try {
    const user   = await fetchUser(id);     // pauses THIS function, not the thread
    const orders = await fetchOrders(user);
    render(orders);
  } catch (err) {                            // normal try/catch works!
    showError(err);
  }
}

Rules that matter:

  • An async function always returns a promisereturn 42 fulfills it with 42; throw rejects it.
  • await unwraps a promise's value, or re-throws its rejection.
  • While awaiting, the function is suspended and the event loop keeps running everything else — await never blocks the thread.
  • Forgetting await is a classic bug: you get a Promise object instead of its value, and errors vanish into an unhandled rejection.

4Running Things in Parallel (and Other Combinators)

Sequential awaits are a hidden performance trap:

// ❌ Sequential — 600ms total if each takes 300ms
const a = await fetchA();
const b = await fetchB();

// ✅ Parallel — ~300ms: start both, then wait for both
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);

The four combinators:

  • Promise.all(promises) — fulfills with an array of results; rejects fast on the first failure.
  • Promise.allSettled(promises) — never rejects; gives {status, value|reason} for each. Use when partial failure is OK.
  • Promise.race(promises) — settles as soon as any settles (win or lose). Classic use: timeouts.
  • Promise.any(promises) — fulfills with the first success, rejects only if all fail.

And remember from the event loop: promise callbacks are microtasks — they run before any pending setTimeout macrotask.

Ready to test yourself?

Sign in to take the quiz, track progress, and earn a certificate.

Sign in