The Event Loop: Microtasks vs Macrotasks
Web developmentweb development / JavaScript
Debug-along. Guess the output before reading on:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Most people say: 1, 2, 3, 4 — "setTimeout(0) runs next, right?"
Actual output: 1, 4, 3, 2.
🧠 Why. JavaScript runs one call stack. When it empties, the event loop decides what runs next — and there are TWO queues, not one:
- Microtask queue — Promise .then/.catch/.finally, await continuations, queueMicrotask.
- Macrotask queue — setTimeout, setInterval, I/O, UI events.
The rule practitioners get wrong: after each macrotask (and after the initial script), the loop DRAINS the entire microtask queue before touching the next macrotask.
So in the trace:
1️⃣ "1" and "4" are synchronous → print now.
2️⃣ Script ends → drain microtasks → "3".
3️⃣ Only now the next macrotask → "2".
setTimeout(…, 0) does NOT mean "as soon as possible." It means "after the current macrotask AND all pending microtasks."
⚠️ The trap that bites in production: microtasks can starve the loop. A .then that schedules another .then, forever, will freeze the page — timers, rendering, and clicks never get a turn:
function loop() { Promise.resolve().then(loop); }
loop(); // UI frozen, but CPU busy
A setTimeout-based loop would NOT freeze the UI, because each iteration is a separate macrotask, so rendering and events run in between.
await is just microtasks in disguise:
async function f() {
console.log("a");
await null; // pause here
console.log("b"); // resumes as a microtask
}
f();
console.log("c");
// Output: a, c, b
Everything after the first await is queued as a microtask — that's why "c" beats "b".
🎯 Mental model: think "flush all microtasks between every macrotask." Node.js follows the same two-queue rule (with extra phases + process.nextTick, which jumps ahead of even Promise microtasks).
Your exercise: predict the exact output, then run it:
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => {
console.log("C");
setTimeout(() => console.log("D"), 0);
});
queueMicrotask(() => console.log("E"));
console.log("F");
Which lines print before the first setTimeout callback fires, and why does D print last?