Back to JavaScript
JavaScript
medium
mid

How does async/await actually work under the hood in JavaScript?

`async` makes a function return a Promise; `await` pauses the function until the awaited Promise settles. Same semantics as `.then` chains, but reads sequentially. Errors become rejected Promises; catch with try/catch or .catch at the call site. Independent awaits should be `Promise.all`'d, not sequential.

4 min read·~8 min to think through

Mental model

async/await is syntactic sugar over Promises. The engine transforms each await into something equivalent to a .then continuation:

js
async function load() {
  const u = await getUser();
  const p = await getPosts(u.id);
  return { u, p };
}

// Roughly equivalent to:
function load() {
  return getUser().then((u) => getPosts(u.id).then((p) => ({ u, p })));
}

Two rules

  1. async functions always return a Promise. Returning a non-Promise wraps it; returning a Promise pass-through.
  2. await only works inside async (or at top-level in ES modules). It evaluates the awaited Promise and returns its value, or throws on rejection.

Errors

js
async function load() {
  try {
    const data = await fetchData();
    return data;
  } catch (err) {
    log(err);
    throw err;   // rejects the returned Promise
  }
}

Throwing inside an async function rejects its Promise. .catch at the call site catches it.

Sequential vs parallel

js
// Sequential — only correct if b depends on a
const a = await fetchA();
const b = await fetchB(a.id);

// Parallel — when they're independent
const [a, b] = await Promise.all([fetchA(), fetchB()]);

Most common perf bug: sequential awaits for independent work.

Loops

js
// Sequential — each iteration awaits before the next
for (const x of arr) await process(x);

// Parallel — all at once
await Promise.all(arr.map((x) => process(x)));

// Limited concurrency — use p-limit or async-pool

Top-level await

In ES modules:

js
// my-module.js (type: module)
const config = await fetch("/config.json").then(r => r.json());
export { config };

Module evaluation pauses until the await settles; importers wait.

Microtask scheduling

await suspends and resumes on the microtask queue:

js
async function f() {
  await null;    // schedules continuation as microtask
  console.log("after");
}
f();
console.log("first");
// "first" then "after"

The await suspends even when the awaited value is already resolved — a microtask hop.

Common mistakes

  • Missing await — returns a Promise instead of a value.
  • Sequential awaits for independent work.
  • async + forEachforEach doesn't wait for the async callbacks; use for..of or Promise.all(map).
  • Not handling rejection — unhandled rejection warning, eventual crash.

Interview framing

"async makes a function return a Promise. await pauses inside that function until the awaited Promise settles, returning its value or throwing on rejection. Equivalent to .then chains but reads top-down. The two biggest gotchas: independent awaits should be Promise.all'd (sequential awaits double latency), and forEach(async ...) doesn't wait — use for..of or Promise.all(map). Errors inside async become Promise rejections; wrap awaits with try/catch or .catch at the call site. Top-level await works in ES modules; module evaluation waits for it."

Follow-up questions

  • Why does Promise.all matter?
  • What's the difference vs raw .then?
  • Why doesn't forEach work with async?

Common mistakes

  • Sequential awaits for independent work.
  • forEach with async callback.
  • Missing await.

Performance considerations

  • Each await is a microtask hop. Parallelize with Promise.all where possible.

Edge cases

  • Top-level await blocks module imports.
  • Async constructors aren't allowed.
  • Awaiting a non-Promise just returns it.

Real-world examples

  • Every modern fetch flow, Node async file I/O, RSC server components.

Senior engineer discussion

Seniors spot sequential awaits in PRs, use Promise.all liberally, and reach for AbortController for cancellation.

Related questions