The Event Loop
The engine finishes the current script, drains every pending microtask, then runs one task from the macrotask queue, on repeat.
Loading visual…
Definition
JavaScript runs on a single thread with one call stack, so it can only execute one piece of code at a time. Asynchronous work, like timers, network responses, and promise resolutions, does not run on that stack while your code is busy; instead the browser or Node schedules the associated callback to run later and holds it in a queue until the stack is free. The event loop is the mechanism that watches the call stack and, once it is empty, decides which queued callback gets to run next. There are two kinds of queues involved. The microtask queue holds jobs like resolved-promise `.then`/`.catch`/`.finally` callbacks and `queueMicrotask` callbacks. The macrotask queue, sometimes called the task or callback queue, holds things like `setTimeout`, `setInterval`, and I/O callbacks. Every time the call stack becomes empty, the event loop first drains the entire microtask queue, running every microtask, including any new ones those microtasks schedule, before it looks at the macrotask queue at all. Only once the microtask queue is completely empty does it pull one macrotask off the queue, run it to completion, and then repeat the cycle: drain microtasks again, then take the next macrotask. This ordering explains a common surprise: a `setTimeout(fn, 0)` does not run immediately after zero milliseconds, it runs after all synchronous code and all microtasks scheduled up to that point, because the timer callback has to wait its turn in the macrotask queue while promise callbacks jump the line in the microtask queue.
Examples
console.log(1);
setTimeout(() => console.log(4), 0);
Promise.resolve().then(() => console.log(3));
console.log(2);
// logs: 1, 2, 3, 41 and 2 run synchronously on the call stack. The Promise callback becomes a microtask and the setTimeout callback becomes a macrotask; once the stack empties, the microtask (3) drains before the single macrotask (4) gets a turn.
Promise.resolve().then(() => {
console.log("microtask A");
Promise.resolve().then(() => console.log("microtask B"));
});
setTimeout(() => console.log("macrotask"), 0);
// logs: microtask A, microtask B, macrotaskMicrotask A schedules a brand new microtask B while it runs. The event loop keeps draining the microtask queue until it is completely empty, so B also runs before the waiting macrotask gets a turn.
Where you see this
- Explaining why a setTimeout(fn, 0) runs after promise callbacks that were scheduled after it.
- Debugging UI updates that seem to lag one tick behind a promise resolution.
- Understanding why a long chain of .then() calls can starve macrotasks like rendering or timers if it never stops producing new microtasks.
- Reasoning about the order of async/await code relative to setTimeout and DOM events.