JavaScript runs on a single thread. It never blocks on I/O — instead, work that finishes later is handed back as a callback that gets queued and run when the stack is free. The rules for which queue and what order are the entire interview answer.
The classic question
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
The output is A, D, C, B — not A, B, C, D, and not A, D, B, C. Step through exactly why:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D'); The three pieces
- Call stack — synchronous code runs here, one frame at a time. The loop can’t do anything else until the stack is empty.
- Macrotask queue (a.k.a. task queue) —
setTimeout,setInterval, I/O, UI events. The loop takes one macrotask per tick. - Microtask queue — Promise callbacks (
.then/.catch/.finally),queueMicrotask,awaitcontinuations. After every task and once the stack empties, the loop drains the entire microtask queue before rendering or taking the next macrotask.
Promise.then (C) always beats setTimeout(0) (B), even though the timer was registered first.The exact algorithm (one loop iteration)
It helps to memorize the loop as a precise procedure:
- Run synchronous code on the call stack until it’s empty.
- Drain the microtask queue completely — running a microtask may enqueue more microtasks, and those run too, in this same checkpoint, before moving on.
- (Browser) Run rendering work if it’s time (style, layout, paint,
requestAnimationFrame). - Take one task from the macrotask queue and run it (back to step 1’s “stack”).
- Repeat.
The single most important consequence: microtasks always fully drain between each macrotask. A “microtask checkpoint” happens whenever the JS stack unwinds to empty — after the initial script, and after each macrotask callback.
Where common APIs land:
| API | Queue |
|---|---|
setTimeout, setInterval, I/O, UI events, MessageChannel | macrotask |
Promise.then/catch/finally, await continuation, queueMicrotask | microtask |
requestAnimationFrame | before paint (its own callback list, not a task/microtask) |
process.nextTick (Node) | runs before the Promise microtask queue |
queueMicrotask vs Promise.then vs setTimeout
queueMicrotask(fn) schedules fn as a microtask directly — same queue as Promise.then, so it beats any setTimeout(0). They’re ordered by insertion within the microtask checkpoint:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Sync first (start, end), then microtasks in insertion order (promise, queueMicrotask), then the macrotask (timeout).
Where await fits
await is syntax sugar over Promises. Everything after an await is scheduled as a microtask — the continuation of the async function.
async function run() {
console.log(1);
await null; // pause; rest becomes a microtask
console.log(3);
}
console.log(0);
run();
console.log(2);
// → 0, 1, 2, 3
0 and 1 are synchronous. run() hits await and yields; the synchronous 2 prints; then the microtask queue runs 3.
async/await desugaring
await expr is roughly Promise.resolve(expr).then(continuation), where the continuation is the rest of the async function. So:
async function f() {
console.log('a');
await x;
console.log('b'); // this whole line+ becomes a .then callback
}
is conceptually:
function f() {
console.log('a');
return Promise.resolve(x).then(() => { console.log('b'); });
}
That’s why everything before the first await runs synchronously, and everything after is a microtask continuation.
More output-ordering puzzles
Puzzle 1 — interleaved sync, micro, macro:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: 1, 5, 3, 4, 2. Sync: 1, 5. Microtask checkpoint drains the whole chain: 3, then its chained .then 4. Then the macrotask: 2.
Puzzle 2 — await ordering between two async functions:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: script-start, a-start, b-start, script-end, a-end. a() runs synchronously through b() (which is fully sync) up to the await; it yields, so script-end prints; then the continuation a-end runs as a microtask.
Puzzle 3 — a microtask scheduled from inside a macrotask:
console.log. Async logs (setTimeout/Promise) appear in real execution order.Answer: initial micro, timeout 1, micro inside timeout, timeout 2. The initial microtask drains first. Then the first timer runs and schedules a microtask; that microtask drains before the next timer, so micro inside timeout beats timeout 2.
Node.js: the same idea, more phases
Node uses libuv and runs its loop in ordered phases, each with its own callback queue: timers (setTimeout/setInterval), pending callbacks, poll (I/O), check (setImmediate), and close callbacks. Between every callback (not just between phases), Node drains microtasks — and within microtasks, process.nextTick runs before the Promise queue.
So the Node priority is: current operation → all process.nextTick callbacks → all Promise microtasks → next phase callback.
// In Node:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');
// → sync, nextTick, promise, then (timeout / immediate order varies)
nextTick beats Promise; both beat any phase callback. (setTimeout(0) vs setImmediate ordering at the top level is non-deterministic; inside an I/O callback, setImmediate always wins.) Warning: like browser microtasks, recursive process.nextTick can starve the loop and block I/O entirely.
Common gotchas
- Assuming
setTimeout(fn, 0)runs “immediately” — it’s a macrotask, so every pending microtask runs first; the minimum delay is also clamped (~4ms for nested timers). - Thinking
awaitmakes code parallel — it suspends the function; subsequent awaits still run after it. - Microtask starvation — endlessly chaining
.then/queueMicrotask/nextTickblocks timers and rendering. - Expecting consistent
setTimeoutvssetImmediateorder in Node at the top level — it isn’t guaranteed. - Long synchronous work blocks the single thread; no queue runs until the stack clears.
Interview questions & model answers
Q: Why does Promise.then run before setTimeout(0)?
.then is a microtask; setTimeout is a macrotask. The loop drains all microtasks after the stack empties, before taking the next macrotask.
Q: What’s a microtask checkpoint? The point — whenever the JS stack unwinds to empty — at which the engine fully drains the microtask queue (including microtasks scheduled during the drain).
Q: Is await blocking?
No. It suspends only the async function and returns control to the loop; the continuation is scheduled as a microtask when the awaited promise settles.
Q: How can a microtask starve the loop? By recursively scheduling more microtasks. Since microtasks fully drain before any macrotask, timers/I/O/rendering never get a turn.
Q: Difference between process.nextTick and queueMicrotask/Promise in Node?
process.nextTick callbacks run before the Promise microtask queue and before the loop continues to the next phase; both run before any timer or I/O callback.
Q: queueMicrotask vs Promise.resolve().then?
Functionally the same queue; queueMicrotask schedules directly without creating a promise, useful when you don’t need a promise value.
Say it out loud (30s)
“JS is single-threaded with an event loop. Synchronous code runs on the call stack. When it empties, the loop drains the entire microtask queue — Promise callbacks and await continuations — and only then takes one macrotask like a setTimeout callback, then drains microtasks again. So in
A; setTimeout(B); Promise.then(C); D, the sync code prints A and D, microtasks print C, and the macrotask prints B last: A, D, C, B. Node adds ordered phases plusprocess.nextTick, which runs before the Promise queue.”