The Event Loop (Microtasks and Macrotasks)
The call stack, and the two queues — microtasks and macrotasks — the event loop drains, always fully emptying microtasks before touching the next macrotask.
Purpose
JavaScript runs on a single thread with a call stack. When the stack empties, the event loop pulls the next unit of work — always draining the entire microtask queue before running even one item from the macrotask queue.
Example
console.log('1: sync');
setTimeout(() => console.log('2: macrotask'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('4: sync');
// Output: 1: sync, 4: sync, 3: microtask, 2: macrotask
Behavior notes
- Microtasks: a resolved/rejected promise’s
.then/.catch/awaitcontinuation,queueMicrotask(). Run immediately after the current synchronous code finishes — every pending microtask, including new ones queued while draining, before anything else. - Macrotasks (tasks):
setTimeout,setInterval, I/O callbacks, UI events. Run one at a time, only once the microtask queue is completely empty. - In a browser specifically, the event loop can also perform a rendering opportunity between macrotasks (but never between microtasks) — part of why long microtask chains can block visual updates even though they don’t “block” in the traditional sense.
setTimeout(fn, 0)never means “run immediately” — it means “run as a macrotask once the stack is empty and all microtasks have drained,” which is always after any already-queued microtask.
Common mistakes
- Assuming a 0ms
setTimeoutruns before a promise continuation scheduled earlier — microtasks always win. - Writing a microtask chain that queues more microtasks indefinitely, which can starve macrotasks (including rendering) from ever running.
Runtime notes
The two-queue model (microtasks vs. macrotasks) is standard across browsers and Node, though the exact catalog of what counts as a macrotask differs slightly between the browser (HTML spec “tasks”) and Node (its own event loop phases).