async / await
Promises that read like ordinary code — plus the parallelism people accidentally throw away.
30 minDifficulty 2/5language · asyncAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
Nested `.then()` and `.catch()` chains solved callback hell, but they fractured control flow into scattered closures and made structured error handling tedious. `async`/`await` flattens asynchronous logic into imperative code that looks synchronous, but developers routinely introduce massive performance bottlenecks by sequentially awaiting independent tasks instead of executing them in parallel.
The mental model
Think of an `async` function as an **automatic bookmark system**. When execution hits `await`, the function places a bookmark at that line, releases the main JavaScript thread to handle other tasks, and returns an unresolved Promise. As soon as the awaited operation resolves, the engine resumes execution from the bookmark.
How it works
Syntax over Promises
An `async` function always returns a `Promise`. If you return a primitive like `42`, the runtime implicitly wraps it in `Promise.resolve(42)`. Inside an `async` function, `await` pauses local execution until the targeted `Promise` settles, unwrapping its resolved value or throwing its rejection as a catchable exception.
Unified Error Handling
Before `async`/`await`, mixing synchronous exceptions with asynchronous rejections required chaining `.catch()` and wrapping initial blocks in `try/catch`. With `await`, both synchronous runtime throws and asynchronous Promise rejections are captured by standard `try { ... } catch (err) { ... }` blocks.
The Sequential Await Antipattern
Writing `const user = await fetchUser(); const posts = await fetchPosts();` creates an accidental waterfall: `fetchPosts` does not initiate until `fetchUser` finishes. When tasks are independent, initiate them simultaneously and await them together using `Promise.all([fetchUser(), fetchPosts()])`.
The mechanism
When an `async` function calls `await <expr>`, the engine evaluates `<expr>` and converts it to a Promise via `Promise.resolve()`. It then attaches the remainder of the async function as a microtask callback and suspends the function frame, returning control to the caller. When the microtask runs on Promise resolution, the engine restores the function frame on the call stack and resumes execution.
sequenceDiagram autonumber participant Main as Main Call Stack participant AsyncFn as Async Function participant Micro as Microtask Queue Main->>AsyncFn: Invokes async function AsyncFn->>Main: Reaches `await promise`, yields control Note over Main: Executes other synchronous code Note over Micro: Promise resolves, queues continuation Main->>AsyncFn: Microtask runs, restores state after `await` AsyncFn->>Main: Function finishes and resolves outer Promise
What people get wrong
- Using `await` blocks the main thread like a synchronous `while` loop.
- `await` is non-blocking to the browser or Node.js runtime; it only pauses execution inside that specific async function frame. The engine frees the call stack immediately upon encountering `await`, allowing the event loop to process user input, rendering, and other asynchronous events while waiting.
- Using `Array.prototype.forEach` with an `async` callback runs items in serial order.
- `forEach` is not Promise-aware and will fire all callbacks concurrently without waiting for them to complete. `forEach` ignores the return value of its callback; use a standard `for...of` loop for sequential iteration or `Promise.all(arr.map(...))` for concurrent execution.
When not to use it
- You need to process a high-frequency stream of events (e.g., mouse moves, WebSocket messages) over time without holding state suspended.
- Use Observables (like RxJS) or Async Iterators (`for await...of`), rather than standard `async`/`await` chains.
- You have multiple independent promises where one failing should not abort the others.
- Use `Promise.allSettled()` instead of a series of raw `await` statements or `Promise.all()`.
Terms
- Syntactic Sugar
- — Syntax within a programming language designed to make things easier to read or express, without adding new underlying runtime capabilities.
- Microtask Queue
- — A high-priority queue in the JavaScript event loop dedicated to processing callbacks from Promises and `await` continuations before the next rendering or macro task.
- Request Waterfall
- — A performance antipattern where network requests execute sequentially one after another, despite having no data dependencies between them.
In an interview
What happens under the hood when a function uses `async` and `await`?
- Explain that `async/await` is syntactic sugar built on top of Promises and Generators.
- Mention that `await` pauses execution of the local function context, not the JavaScript thread.
- Highlight that resume steps are queued onto the Microtask Queue once the Promise settles.
How do you handle error handling with `async`/`await` compared to raw Promises?
- Explain the use of standard `try/catch/finally` blocks.
- Mention that `try/catch` catches both synchronous errors and rejected Promises within the scope.
- Discuss unhandled rejection risks if an `async` function is invoked without `await` or `.catch()`.
Can you recall it?
What is the key performance hazard when using `async`/`await` across multiple independent network calls, and how do you resolve it?