JavaScript

From callbacks to async/await

What a promise actually is, why await only pauses the function it is in, and the try/catch that never catches.

After this lesson you can

  • Explain what state a promise is in and how it gets there
  • Say why an unhandled rejection is different from a thrown error
  • Spot a promise that was never awaited

A promise is an object representing a value that is not ready yet. It is always in exactly one of three states: pending, fulfilled with a value, or rejected with a reason — and once it leaves pending, it never changes state again.

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

wait(100).then(() => console.log("done"));

async/await is syntax over the same promises — it does not replace them, it makes them readable.

async function load() {
  const res = await fetch("/api/users/1");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

An async function always returns a promise, whatever you return inside it. await pauses that function until the promise settles; it does not pause the rest of the program, which keeps running.

Try it

Same work, one third of the waitnode-22
function wait(ms) {  return new Promise((resolve) => setTimeout(resolve, ms));} export async function run() {  const sequentialStart = Date.now();  await wait(60);  await wait(60);  await wait(60);  const sequential = Date.now() - sequentialStart;   const parallelStart = Date.now();  await Promise.all([wait(60), wait(60), wait(60)]);  const parallel = Date.now() - parallelStart;   return {    sequentialMs: Math.round(sequential / 10) * 10,    parallelMs: Math.round(parallel / 10) * 10,  };}
What to look for

The try/catch that never catches

try {
  sendWelcomeEmail(user.email);   // no await
} catch (err) {
  logger.warn(err);                // never runs
}

try only guards the synchronous part of its block. The call returns a promise immediately, the block finishes, and the rejection arrives later with nothing on the stack to catch it — an unhandled rejection, which under Node's default policy can crash the process.

The fix is await:

try {
  await sendWelcomeEmail(user.email);
} catch (err) {
  logger.warn(err);                 // now it actually runs
}

The habit worth building: every promise is awaited, returned, or explicitly discarded with a comment saying why it is fire-and-forget. A bare promise-returning call sitting on its own line as a statement always deserves a second look.

Sequential awaits waste the wait

const a = await loadUser(id);      // waits
const b = await loadOrders(id);    // then waits again, even though
                                     // loadOrders never needed `a`

If two awaited calls do not depend on each other, awaiting them one after another adds their times together for no reason. Promise.all starts both immediately and waits once:

const [a, b] = await Promise.all([loadUser(id), loadOrders(id)]);

Promise.all rejects as soon as any one promise rejects, and cancels nothing — the others keep running in the background. Promise.allSettled is for when you want every outcome, failures included, rather than the first failure stopping everything.

Try it yourself

2 visible tests · 2 hidden tests

attempt(id) is given: it resolves to { id, ok: true } for an even id, and rejects with `failed: ${id}` for an odd one. Implement runAll(ids), which calls attempt for every id in parallel and returns an array of results in the same order — { id, ok: true } for a success, { id, ok: false } for a failure. One failure must not stop the others from being reported.

  • runAll([1,2,3,4])
  • runAll([2,4,6])
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up