The Event Loop
The event loop is the small algorithm at the heart of every JavaScript runtime. It is how a single thread juggles timers, fetches, user input and animation frames without blocking. Once you can picture it, surprising orderings of setTimeout and Promise callbacks stop being surprising — they become predictable.
The cast of characters
The runtime has four pieces working together. None of them are JavaScript code — they live in the browser or in Node.
Call stack — the engine pushes a frame for every function call and pops it on return. While the stack has frames, JavaScript is "busy".
Heap — where objects, arrays, closures and DOM nodes live. Stack frames hold references into the heap.
Task queue (a.k.a. macrotask queue) — callbacks waiting to run:
setTimeout, I/O completion, message events.Microtask queue — a higher-priority queue for promise reactions and
queueMicrotaskcallbacks.
Plus, in the browser, a render step the engine wants to run roughly 60 times per second to repaint the page.
The loop, in words
The event loop runs forever. Each turn looks roughly like this:
Pick one task from the task queue and run it to completion (no other task can interrupt it).
Drain the microtask queue — run every microtask, including ones added by other microtasks.
If a render is needed (browser only), run
requestAnimationFramecallbacks, then layout and paint.Go back to step 1.
The classic ordering example
console.log("1: script start");
setTimeout(() => console.log("2: timeout"), 0);
Promise.resolve().then(() => console.log("3: microtask"));
console.log("4: script end");1: script start 4: script end 3: microtask 2: timeout
Why this order?
The top-level script runs as one task. It logs "1", schedules a timeout (task), schedules a microtask, logs "4".
The task finishes. The event loop drains microtasks before the next task — "3" prints.
Now the queue moves on to the timeout task — "2" prints.
Where each API queues
Microtasks —
Promise.then/catch/finally,queueMicrotask,MutationObservercallbacks,awaitcontinuations.Macrotasks (tasks) —
setTimeout,setInterval,setImmediate(Node),MessageChannel, I/O completion, UI events like clicks.Animation frame —
requestAnimationFrame(browser only). Runs once per frame, between tasks and the paint.
See the next page for a deeper comparison of these queues.
A more involved example
console.log("A");
setTimeout(() => {
console.log("B");
Promise.resolve().then(() => console.log("C"));
}, 0);
Promise.resolve().then(() => {
console.log("D");
setTimeout(() => console.log("E"), 0);
});
console.log("F");A F D B C E
Walking through it:
The script is one task. It logs A, queues a timeout (task), queues a microtask, logs F.
Script finishes — drain microtasks. The microtask logs D and queues another timeout. End of microtasks.
Next task is the first timeout. It logs B and queues a microtask. Task finishes — drain microtasks: C.
Final task is the second timeout. It logs E.
Why setTimeout(fn, 0) is not really 0
setTimeout(fn, 0) says "as soon as possible, but in a new task". It still waits for the current task to finish, all microtasks to drain, and maybe a render. Browsers also enforce a minimum delay (commonly 4 ms for nested timers, longer for background tabs).
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log("timeout", i), 0);
Promise.resolve().then(() => console.log("microtask", i));
}microtask 0 microtask 1 microtask 2 microtask 3 microtask 4 timeout 0 timeout 1 timeout 2 timeout 3 timeout 4
Every microtask scheduled during the synchronous loop runs before any of the timeouts.
Long microtask chains can starve tasks
Because all microtasks drain before the next macrotask, an infinite chain of microtasks can block timeouts and even rendering. This is rare, but a recursive Promise.resolve().then(...) that re-queues itself will starve the UI.
// Don't do this in real code — it locks the page.
function spin() {
Promise.resolve().then(spin);
}
// spin(); // browser becomes unresponsiveNode has more queues
Node's event loop has several phases in each tick — timers, pending callbacks, idle/prepare, poll (I/O), check (setImmediate), close handlers — with microtasks (and process.nextTick, which is even higher priority) draining between them. The browser model is a useful simplification; Node adds nuance you only need when you debug subtle timing bugs.