Microtasks & Macrotasks
Two queues, one loop
Section titled “Two queues, one loop”The event loop processes async callbacks from two distinct queues with different priorities:
Microtask queue — processed after every task, before the next macrotask. Sources:
- Promise
.then/.catch/.finallycallbacks queueMicrotask(fn)
Macrotask queue (also called the task queue) — one task is processed per event loop turn. Sources:
setTimeoutcallbackssetIntervalcallbacks- I/O callbacks (network, file system)
The rule is simple: after each macrotask completes, the engine drains the entire microtask queue before picking up the next macrotask.
The drain order
Section titled “The drain order”┌─────────────────────────────┐│ Execute one macrotask ││ (or the initial script) │└──────────────┬──────────────┘ │ ▼┌─────────────────────────────┐│ Drain all microtasks │◄──┐│ (including newly added) │ │ new microtasks└──────────────┬──────────────┘───┘ │ ▼┌─────────────────────────────┐│ Pick next macrotask │└─────────────────────────────┘Node-specific: process.nextTick and setImmediate
Section titled “Node-specific: process.nextTick and setImmediate”Node.js adds two more scheduling APIs that require the Node runtime:
// These only work in Node.js — not in browsersprocess.nextTick(function() { console.log('nextTick fires before other microtasks');});
setImmediate(function() { console.log('setImmediate fires after I/O callbacks in the current loop iteration');});process.nextTick callbacks are drained before the microtask queue. setImmediate fires in the check phase of Node’s event loop. Because these require the Node runtime, they are not available in the in-browser runner below — run them locally or in StackBlitz.
Runnable ordering demo
Section titled “Runnable ordering demo”This demo prints four lines. Predict the order before running:
- The synchronous
console.logruns immediately on the call stack. setTimeout(..., 0)queues a macrotask.Promise.resolve().then(...)queues a microtask (the Promise is already resolved, so the reaction is queued synchronously).queueMicrotask(...)queues another microtask — after the Promise reaction already queued above.
After the synchronous script finishes, all microtasks drain in order, then the macrotask fires.
The output is always in this order:
1 sync— call stack2 microtask (Promise)— first microtask queued3 microtask (queueMicrotask)— second microtask queued4 macrotask— runs only after the microtask queue is empty