JavaScript Event Loop
Prompt
Given a chunk of JS that mixes a setTimeout, a resolved promise's .then, and some
synchronous code, walk me through the exact order things run — and explain the rule that
decides it.
How this round runs
This is a conversation that escalates — I'll keep asking "and what happens when…" until I
find the edge of what you know. I want the micro-task vs. macro-task ordering rule stated
precisely, and I'll push into starving the loop and the subtle behavior of await inside a
loop — reason it out rather than guessing.
Model answer
I'll state the ordering rule, then run an example against it. The engine runs synchronous
code on the call stack to completion first. Async results arrive as queued jobs in two
tiers: the micro-task queue (promise reactions — .then, the continuation after an
await, queueMicrotask) and the macro-task queue (setTimeout, setInterval, I/O
callbacks). The rule that decides order: after the current task finishes, the loop drains the
entire micro-task queue — including micro-tasks scheduled by other micro-tasks — before it
ever picks up the next macro-task. So micro-tasks always jump ahead of the next timer.
Run it: synchronous code runs first; a setTimeout(fn, 0) registers fn as a macro-task; a
Promise.resolve().then(g) registers g as a micro-task. When the stack clears, the loop
drains micro-tasks, so g runs before fn, even with a 0 ms timeout — because the
timeout only governs eligibility, and the macro-task isn't considered until the micro-task
queue is empty. That ordering is the thing people get wrong, and it's deterministic, not a
race.
The edges. Starving the loop: because the loop drains micro-tasks fully before the next
macro-task, a micro-task that keeps scheduling more micro-tasks (or a long synchronous block)
never yields — timers and I/O callbacks are starved and the UI freezes; that's why heavy CPU
work belongs on a worker, not the main thread. await inside a loop is the other trap, and
it's two distinct cases: for…of with an await serializes — each iteration suspends and
resumes before the next starts, so N awaited calls run one after another; if they're
independent you usually want to start them all and await Promise.all(...) to overlap the
I/O. My honest boundary: the macro-task families (timers vs. I/O vs. setImmediate) have a
defined phase order in Node that differs from the browser's model, and I'd check the
runtime's phase diagram before asserting cross-task ordering beyond the micro/macro rule.
- Stated the rule precisely: drain the ENTIRE micro-task queue before the next macro-task
- Ran a concrete example and explained why .then beats setTimeout(0) — eligibility vs. scheduling
- Connected it to a real consequence: deterministic ordering, not a race
- Diagnosed starvation correctly — full micro-task drain means a micro-task loop or sync block freezes everything
- Got await-in-a-loop right: for…of serializes; Promise.all overlaps; honest that Node phase order differs from the browser
- And what happens when a micro-task keeps queuing more micro-tasks? → the loop never reaches the next macro-task; timers/I-O starve, UI freezes
- You await inside a for…of over ten independent requests — fast or slow, and why? → serialized, ten round trips back-to-back; use Promise.all to overlap them
- Promise.then vs setTimeout(0) — which runs first and what's the underlying reason? → the then; micro-tasks drain fully before any macro-task is considered