Microtasks and the event loop
Why promise callbacks run before timers — the event loop in detail.
- Distinguish the macrotask and microtask queues
- Predict the order of sync, promise, and timer callbacks
- Avoid blocking the event loop
The async lesson introduced the event loop. This goes one level deeper, into a detail that explains a lot of surprising ordering: JavaScript has two queues of pending work, and they don't have equal priority.
Two queues
After each piece of synchronous code finishes, the event loop drains work in a specific order:
- Microtasks — promise callbacks (
.then, the continuation afterawait),queueMicrotask. The loop drains the entire microtask queue before doing anything else. - Macrotasks —
setTimeout, I/O callbacks, events. Only one is processed per loop iteration, after all microtasks are clear.
So promises always run ahead of timers, even setTimeout(fn, 0).
Predicting the order
This is the classic interview puzzle — and now it makes sense:
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
// prints: 1, 4, 3, 21 and 4 are synchronous. Then the loop drains microtasks (3) before taking
the timer macrotask (2). Promise continuations always win the race against
timers.
Don't block the loop
Because it's all one thread (the async lesson), a long synchronous computation freezes everything — no other callback, no rendering, nothing runs until it finishes:
// Blocks the whole thread; the UI is frozen for the duration.
for (let i = 0; i < 5_000_000_000; i++) {}For heavy CPU work, move it off the main thread — into a Web Worker (the same mechanism the in-browser runners on this site use). The main thread stays responsive while the worker computes.
An endless chain of microtasks can starve macrotasks — if promise callbacks keep scheduling more promise callbacks, timers and rendering never get a turn. Order matters, and so does not monopolising either queue.
Where to go next
The event loop governs when code runs; memory governs how long values live. Next: memory and garbage collection.