async/await: Generator De-Sugaring & Microtask Queue Steps
- Last updated
- Prerequisites:
- Promises and the microtask queue
- Understanding of generator functions and yield
- async-await
- generators
- state-machine
- microtask
- promises
- control-flow
Read at your depth
The practical view
async functions always return a Promise. Inside, await pauses the function until the awaited promise settles, then resumes with the resolved value (or throws on rejection, caught by try/catch). async function getData() { const res = await fetch(url); return res.json(); } is the modern replacement for .then chains. Await works on any thenable and even non-promises (they are wrapped). Async functions are built on generators + promises under the hood; each await is a suspension point. Use await in loops and conditionals naturally — no more promise chains or nesting. Remember: an unhandled rejection inside an async function without try/catch rejects the returned promise.
The same idea in other frameworks
React data fetching with async/await inside effects; Suspense integrates with promise-based loading.Angular services use async/await with HttpClient promises; RxJS toPromise bridges to await.Vue composables expose async setup logic; await in setup() is supported in Composition API.SvelteKit load functions are async; the framework awaits them during SSR/hydration.Legacy vs modern
Promise chain vs async/await for sequential async steps
async/await reads like synchronous code and shares the same microtask machinery, avoiding nested .then callbacks and manual error handling.
fetch(url).then(r => r.json()).then(data => process(data)).catch(err => handle(err)); // ❌ OLD: linear but callback-heavyasync function load(url) {
const r = await fetch(url); // ✅ NEW: suspension point, reads like sync
return process(await r.json());
}Interview gotchas
Context
Interviewers want the scheduling mechanics — that await resumption is a promise reaction enqueued as a microtask — which determines observable ordering in real apps.
The mechanical answer
An async function runs synchronously until its first await. The awaited promise is then processed via PerformPromiseThen, which registers a reaction (resume function). When the awaited promise settles, that reaction is enqueued on the microtask queue, not the macrotask queue. The event loop drains the microtask queue at checkpoints (after the current task and script), so resumption happens as soon as the current task completes — before any timers or I/O macrotasks. Because each await boundary enqueues a fresh microtask, an async function with multiple awaits interleaves with other promise-based work but never 'jumps the queue' ahead of timers. This is why awaiting in a loop yields control between iterations.
Trap
The naive answer is 'await returns control to the event loop as a new task'. Await resumes in the microtask queue, which has higher priority than tasks. Another trap: assuming await polls or blocks — it suspends the state machine and releases the thread; blocking happens only with Atomics.wait or synchronous busy loops. Candidates who name the promise reaction + microtask enqueue show they understand the mechanism.
Context
This probes the state-machine/context mechanics of async functions — that suspended state is heap-allocated, not on the stack.
The mechanical answer
When an async function suspends at an await, its execution context is saved so it can resume later. In V8, the compiler allocates the function's local variables in a heap-allocated context (or a dedicated suspended-frame object) rather than plain stack slots, because the stack frame is popped when the function yields to the caller. The saved state includes the current state index (which await we're at) and the live locals. On resumption, the state machine restores that context and continues from the saved state. This is why closures and async functions both rely on heap-allocated environments, and why capturing large objects in long-lived async closures can retain memory.
Trap
The naive answer is 'locals stay in the stack frame until the promise resolves'. The stack frame is gone the moment the function yields; locals survive in the heap context. Another trap: assuming each await creates a new function call on the stack — it is a state-machine resume, not a recursive call, so deep await chains do not grow the call stack. Candidates who mention the saved context and state index demonstrate real understanding.