Microtasks vs. Macrotasks
Microtasks, like promise callbacks, always run before the next macrotask, like a setTimeout, even a zero-delay one.
Loading visual…
Definition
The event loop pulls work from two different queues. The macrotask queue, sometimes called the task or callback queue, holds things like setTimeout and setInterval callbacks, UI events, and I/O callbacks. The microtask queue holds promise callbacks, `.then`, `.catch`, `.finally`, and jobs scheduled with `queueMicrotask`. The two queues are not treated equally. After the currently running script finishes and the call stack empties, the event loop drains the microtask queue completely, running every microtask currently in it, plus any new ones those microtasks add along the way, before it is allowed to touch the macrotask queue. Only when the microtask queue is fully empty does the loop take a single macrotask, run it, and then drain microtasks again before considering another macrotask. This is why `Promise.resolve().then(fn)` reliably runs before `setTimeout(fn, 0)`, even though both were scheduled at essentially the same moment: the promise callback goes into the higher-priority microtask queue, while the timer callback has to wait in the macrotask queue behind every pending microtask.
Examples
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
// logs: promise, timeoutBoth are scheduled back to back, but the promise callback is a microtask and always drains before the timer callback, a macrotask, gets its turn.
queueMicrotask(() => console.log("micro"));
setTimeout(() => console.log("macro"), 0);
console.log("sync");
// logs: sync, micro, macroThe synchronous log runs first. Then the microtask queue drains (micro), and only after it is empty does the event loop take the setTimeout callback (macro).
Where you see this
- Predicting whether a setTimeout(fn, 0) will run before or after a chain of .then() callbacks.
- Using queueMicrotask to defer work until right after the current synchronous code, but before any timers or rendering.
- Debugging why UI updates queued in a promise chain appear before a scheduled timer fires.
- Understanding why a runaway chain of microtasks can delay timers and I/O from ever getting a turn.