Engrain by ByteRush

JS.01.05 Mid-level· 10 minCore — asked often

async/await in the event loop: what really happens at each await

await pauses only the async function and resumes it in a microtask. See why code after await runs later, and why forEach does not wait.

Comes up in: JavaScript, Coding

Learn#

Kabir writes items.forEach(async (item) => await save(item)) and then shows “All saved!”. Users see the message before anything is saved. To fix it, you need to know what await does to the event loop.

await pauses only the current async function. That is called suspension: its frame leaves the call stack and the rest of the function — the continuation — is saved. Everything else keeps running. When the awaited value is ready, the continuation is queued as a microtask.

An async function returns a promise as soon as it first pauses. Its return value settles that promise later.

Even await on a plain value like 42 pauses until the next microtask checkpoint.

async function load() {
  console.log("2 load starts");
  const x = await 42;
  console.log("4 resumed with", x);
  return "done";
}
console.log("1 script");
const p = load();
console.log("3 script continues", p instanceof Promise);
p.then((v) => console.log("5", v));
setTimeout(() => console.log("6 timer"), 0);

Output (verified)

1 script
2 load starts
3 script continues true
4 resumed with 42
5 done
6 timer

So forEach never waits: each callback pauses at await and returns a promise that forEach ignores. Use for...of with await for one-at-a-time work.

await pauses one function, not the programStep 1 of 5

Call stack

  • script
  • load

Microtasks

    empty

    Console

    • 1 script
    • 2 load starts

    The script calls load(). Its body runs synchronously until the first await.

    Read the steps as text
    1. The script calls load(). Its body runs synchronously until the first await.
    2. await pauses load. Its frame leaves the stack; the rest of load is saved for later.
    3. load already returned a pending promise, so the script carries on.
    4. The script ends. The microtask resumes load, which prints and returns 'done'.
    5. Returning settles load's promise, so its .then callback runs next. The timer, a task, comes last.

    Predict#

    Predict first

    What does this print? Type the letters with spaces.

    async function f() {
      console.log("B");
      await null;
      console.log("D");
    }
    console.log("A");
    f();
    console.log("C");

    Interview angle#

    Say it like this — about 30 seconds

    “An async function runs synchronously until its first await. Then it suspends: its frame leaves the stack, the rest is saved as a continuation, and the caller gets a pending promise. When the awaited promise settles, the continuation is queued as a microtask and resumes there. So code after await always runs later, even for await null. A return resolves the function’s promise. That is why forEach with an async callback does not wait.”

    Stays on your device. Nothing is uploaded.
    Follow-up What does this print: console.log(1); setTimeout(() => console.log(2)); Promise.resolve().then(() => console.log(3)); console.log(4);
    1, 4, 3, 2. The synchronous logs run first, the promise callback is a microtask, and the timer is a task that runs after all microtasks.
    Follow-up How is await built on promises?
    await x turns x into a promise and attaches the continuation with the same machinery as .then, so resuming is a promise job — a microtask.
    Follow-up How do you run async work in parallel instead of in series?
    Start all the promises first, then await Promise.all(promises) — or use a pool for a limit (JS.11.07).

    Practice#

    faded

    Fix mapSeries: it must process items one at a time, in order, and resolve only when all are done.

    Done when

    • The results array matches the input order and is complete when the promise resolves.
    • Only one call to fn runs at a time, and items start in input order.
    Your solution
    Ctrl/⌘ + Enter

    Remember#

    Try to answer each card before you open it. Then add them to your reviews.

    What does await pause?

    Only the current async function. The rest of the program keeps running.

    How does an async function resume after await?

    Its continuation is queued as a microtask when the awaited value settles.

    Why does forEach not wait for async callbacks?

    It ignores the promises the callbacks return.

    What does this print?
    (async () => { console.log("x"); await 0; console.log("z"); })();
    console.log("y");
    x
    y
    z

    Go deeper

    Four layers. Stop at the one your interview needs; each ends with a quick check.

    The core ideaEvery round

    An async function runs like normal code until await. There it hands control back, and the caller gets a promise. The rest of the function resumes in a microtask once the value is ready.

    Check yourself: Does code after `await null` run in the same task?

    Yes, but later in it — at the microtask checkpoint, after the current code finishes.

    How it works underneathSenior rounds

    The spec’s Await step converts the value to a promise and attaches the continuation as a promise reaction Source: ECMA-262: Await. So resuming is a promise job, run at the microtask checkpoint like any .then callback.

    Check yourself: Which machinery does await reuse to resume?

    Promise reactions — the same jobs that run .then callbacks.

    Edge cases and trapsWhere people slip

    Awaiting a native promise used to cost three microtask ticks. A 2018 change to the spec cut it to one, so every engine now behaves like this Source: V8 blog: Faster async functions and promises:

    const p = Promise.resolve();
    (async () => { await p; console.log("after await"); })();
    p.then(() => console.log("then 1")).then(() => console.log("then 2"));

    Output (verified)

    after await
    then 1
    then 2

    V8 shipped it first, in version 7.2.

    Check yourself: Why do old blog posts show `then 1, then 2` printing before `after await`?

    Before a 2018 spec change, await on a native promise took three microtask ticks; now it takes one.

    In productionStaff-level answers

    Unawaited promises cause “saved” messages before saving and errors nobody catches. Turn on @typescript-eslint/no-floating-promises and no-misused-promises. Use for...of with await for order, Promise.all for speed, and a pool when an API limits you.

    Check yourself: Name one tool that fails your build when you forget to await a promise.

    The TypeScript ESLint rule no-floating-promises.

    Finished the five steps? Mark it, and its cards join your reviews.

    Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

    Interview questions this answers

    • What is the output of: console.log(1); setTimeout(()=>console.log(2)); Promise.resolve().then(()=>console.log(3)); console.log(4); and why?
    • How does await work under the hood in terms of the event loop?