JavaScript Event Loop: Microtasks vs. Macrotasks
The response explains how JavaScript's event loop schedules microtasks and macrotasks, with a worked example showing the output order of setTimeout, Promise.resolve().then, and queueMicrotask calls.
A read-only, creator-approved conversation screened before publication.
Explain how JavaScript's event loop schedules microtasks versus macrotasks, with a worked example tracing the exact output order of setTimeout, Promise.resolve().then, and queueMicrotask calls.
THE MODEL The event loop is a single-threaded loop. Each iteration pulls one macrotask (task) off the macrotask queue, runs it, then drains the ENTIRE microtask queue before pulling the next macrotask. That "fully drain microtasks after every task" rule is the whole game. QUEUE CONTENTS Macrotasks (task queue): setTimeout, setInterval, setImmediate (Node), I/O callbacks, UI events, the main script itself. Microtasks: Promise.then/catch/finally, queueMicrotask, MutationObserver, process.nextTick (Node — runs before other microtasks). WORKED EXAMPLE console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); queueMicrotask(() => console.log('D')); Promise.resolve().then(() => { console.log('E'); queueMicrotask(() => console.log('F')); }); setTimeout(() => { console.log('G'); Promise.resolve().then(() => console.log('H')); }, 0); console.log('I'); STEP-BY-STEP TRACE Step 1 — the main script runs as the first macrotask: logs A registers macrotask B (setTimeout) registers microtask C registers microtask D registers microtask E (which will later queue F) registers macrotask G (setTimeout) logs I Output so far: A I Step 2 — script finishes, so the loop drains the microtask queue completely: C runs -> logs C D runs -> logs D E runs -> logs E, and queues microtask F F runs -> logs F (queued DURING the drain, so it still runs now, not later) Output so far: A I C D E F Microtask queue is now empty. Step 3 — next macrotask: B (setTimeout 0). logs B Output so far: A I C D E F B B queued no microtasks, so nothing to drain. Step 4 — next macrotask: G (setTimeout 0). logs G, then queues microtask H Output so far: A I C D E F B G Step 5 — G finishes, drain microtasks: H runs -> logs H Output so far: A I C D E F B G H FINAL OUTPUT A I C D E F B G H WHY THIS ORDER (THE THREE RULES) 1. Synchronous code (A, I) always beats everything — it's the current task. 2. Microtasks (C, D, E, F) always beat the next macrotask, because the queue is drained after every single task. This is why C and D beat B even though B was "scheduled" first with a 0ms timer. 3. A microtask that schedules another microtask (E -> F) keeps the drain going — F runs before B. Microtasks do NOT yield back to macrotasks between themselves; the drain only stops when the microtask queue is empty. Practical consequence: setTimeout(0) is never a reliable "run this after the current synchronous block" tool — microtasks will always jump ahead of it. Use queueMicrotask or a resolved Promise when you need something to run right after the current code but before any timer/I-O callback.
RELATED SIGNALS
Understanding Web Page Load Times: A Detailed Breakdown
The response explains the breakdown of a web page's load time, including DNS, TCP, TLS, TTFB, and render-blocking resources, and discusses t
READ TRANSMISSION →
Sunk Cost Fallacy and Escalation of Commitment: Historical Examples and Decision
The response explains the sunk cost fallacy and escalation of commitment using the Concorde and Motorola Iridium projects as examples, and p
READ TRANSMISSION →
Understanding Randomized Controlled Trials: A Historical Medical Example
The response explains randomized controlled trials using the CAST trial as an example, discussing randomization, control groups, p-hacking,
READ TRANSMISSION →