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.
Common mistakes
- Assuming setTimeout(fn, 0) runs right away; it still has to wait for the call stack to empty and for the entire microtask queue to drain first.
- Thinking the event loop runs one microtask and then checks the macrotask queue; it actually drains the whole microtask queue, including new microtasks scheduled along the way, before touching a single macrotask.
- Writing a chain of promises that keeps scheduling more microtasks and being surprised that timers or rendering never get a turn.
Key takeaways
- JavaScript has one call stack, so only one piece of code runs at a time.
- Once the stack is empty, the entire microtask queue drains before the event loop runs even one macrotask.
- New microtasks scheduled while draining still run in that same pass, ahead of any macrotask.
- This is why a resolved promise's .then always logs before a setTimeout(fn, 0).
What does this log? console.log(1); setTimeout(() => console.log(4), 0); Promise.resolve().then(() => console.log(3)); console.log(2);
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.