How async/await works 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.
Mental model
async/await is syntactic sugar over Promises. The engine transforms each await into something equivalent to a .then continuation:
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
asyncfunctions always return a Promise. Returning a non-Promise wraps it; returning a Promise pass-through.awaitonly works inside async (or at top-level in ES modules). It evaluates the awaited Promise and returns its value, or throws on rejection.
Errors
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
// 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
// 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-poolTop-level await
In ES modules:
// 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:
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+forEach—forEachdoesn't wait for the async callbacks; usefor..oforPromise.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.