async / await

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

What if promise code could read from top to bottom again? async and await do not replace promises. They give you kinder syntax for the same future-result machine, so your async code can look like the story it tells.

async Functions Return Promises

An async function is a function marked with async that always returns a promise. Even if you return a plain value, JavaScript wraps that value in a fulfilled promise.

Inside an async function, await pauses that async function until a promise settles, then gives you the fulfilled value. Think of await as opening the pickup receipt from lesson 12.2: if the order is ready, you get the food; if it failed, you get the error.

function loadScore(player, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ player, score: 46 });
    }, delay);
  });
}
 
async function showScore() {
  console.log("inside async function");
 
  const record = await loadScore("Mina", 10);
  console.log(`${record.player}: ${record.score}`);
 
  return `${record.player} displayed`;
}
 
const resultPromise = showScore();
 
console.log("after showScore call");
 
resultPromise.then(message => {
  console.log(message);
});

inside async function, after showScore call, Mina: 46, then Mina displayed.

showScore() starts running right away. It reaches await, returns a pending promise to the caller, and lets the rest of the script continue. Later, the awaited promise fulfills, and showScore resumes.

Try it — read async code top to bottom

This dashboard has a real order: you need the score before choosing the badge. Run it, then notice the two separate facts: the caller keeps going after showDashboard("Mina"), and the async function resumes later where it left off.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

The final expression is the promise returned by the async function. In the playground, that makes the pending promise visible first, then its settled value after the logs finish.

try/catch Works With await

When an awaited promise rejects, await throws that rejection inside the async function. That means the try/catch structure from lesson 9.1 finally works with promise failures.

function reserveSeat(player, seatAvailable, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!seatAvailable) {
        reject(new Error(`${player} has no seat`));
        return;
      }
 
      resolve(`${player} reserved seat A3`);
    }, delay);
  });
}
 
async function printReservation(player, seatAvailable, delay) {
  try {
    const message = await reserveSeat(player, seatAvailable, delay);
    console.log(message);
  } catch (error) {
    console.log(`caught: ${error.message}`);
  }
}
 
printReservation("Rae", true, 10);
printReservation("Kai", false, 20);

Rae reserved seat A3, then caught: Kai has no seat.

The catch block catches the rejection because the rejection happens at the await line. A plain try around an old async callback still would not catch errors thrown later inside that callback; promises and await give the error a path back into your function.

The Quiet Serialization Bug

Serialization means doing async jobs one after another. Sometimes that is correct: choosing a badge after loading a score depends on the score. But if jobs do not depend on each other, accidental serialization wastes time.

Parallel means starting async waits together so their waiting time overlaps. JavaScript still runs your code on one thread; the runtime is just waiting on several timers at once.

function loadScore(player, delay) {
  console.log(`request sent: ${player}`);
 
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`response: ${player}`);
    }, delay);
  });
}
 
async function printScoresSlowly() {
  console.log("slow loop");
 
  const requests = [
    { player: "Mina", delay: 30 },
    { player: "Rae", delay: 10 },
    { player: "Nico", delay: 20 },
  ];
 
  for (const request of requests) {
    const message = await loadScore(request.player, request.delay);
    console.log(message);
  }
 
  console.log("slow done");
}
 
async function printScoresStartedTogether() {
  console.log("started together");
 
  const minaScore = loadScore("Mina", 30);
  const raeScore = loadScore("Rae", 10);
  const nicoScore = loadScore("Nico", 20);
 
  console.log("all requests sent");
  console.log(await raeScore);
  console.log(await nicoScore);
  console.log(await minaScore);
  console.log("together done");
}
 
async function run() {
  await printScoresSlowly();
  await printScoresStartedTogether();
}
 
run();

slow loop, request sent: Mina, response: Mina, request sent: Rae, response: Rae, request sent: Nico, response: Nico, slow done, started together, request sent: Mina, request sent: Rae, request sent: Nico, all requests sent, response: Rae, response: Nico, response: Mina, then together done.

The exact wrong output in the slow loop is request sent: Rae appearing only after response: Mina. Rae's faster 10ms request never even starts until Mina's 30ms request finishes. When you mean overlap, start the promises first, then await them later. Lesson 12.4 gives you Promise.all, the normal tool for collecting those results cleanly.

Top-Level await Belongs in Modules

Top-level await is await used outside an async function in a module. A module is a JavaScript file loaded with module rules, with its own top-level scope and support for import and export.

Top-level await is useful for startup work: load configuration, prepare a connection, then let the rest of the module continue.

function loadStartupSettings(delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ player: "Mina", theme: "dark" });
    }, delay);
  });
}
 
console.log("module boot");
 
const settings = await loadStartupSettings(10);
 
console.log(`${settings.player} theme: ${settings.theme}`);
console.log("module ready");

module boot, Mina theme: dark, then module ready.

Use top-level await for module setup, not for every line that happens near the top of a file. Inside ordinary functions, keep using async function and await where the waiting belongs.

You now have the promise machine in its most readable form: future results, errors, and straight-line async code. Next, promise combinators give you the professional tools for starting many promises and deciding how their results should combine.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion