Last 30 Days
No notifications
How does single-threaded JS handle thousands of concurrent timers, network calls, and clicks? The event loop. This page covers the mental model — call stack, Web APIs / libuv, the macrotask queue, and the higher-priority microtask queue — and finally explains why setTimeout(fn, 0) runs *after* Promise.resolve().then(fn).
# The Event Loop
setTimeout, fetch, DOM events, file I/O. These run OUTSIDE the JS engine.
3. Macrotask queue (a.k.a. *task queue* / *callback queue*) — setTimeout, setInterval, I/O, UI events.
4. Microtask queue — Promise.then, queueMicrotask, MutationObserver. Higher priority.
5. The loop: when the call stack is empty,
- drain ALL microtasks,
- then take ONE macrotask,
- then drain ALL microtasks again,
- render (in browsers, between iterations),
- repeat.console.log("A");setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Output: A, D, C, B.Why?
1. A — sync.
2. setTimeout schedules a *macrotask*.
3. Promise.then schedules a *microtask*.
4. D — sync.
5. Stack empty → drain microtasks → C.
6. Take next macrotask → B.
So:
Promise.resolve().then(…) runs *before* the next setTimeout(0)..then can starve macrotasks (and rendering!) — but it's rare in practice.queueMicrotask(() => console.log("micro"));
Same priority as Promise callbacks — useful when you want "after current code, but before any timer / I/O".setTimeout(fn, 0) isn't really 0queueMicrotask is faster and runs sooner.const fs = require("fs");console.log("start");
fs.readFile("./big.txt", () => console.log("file done"));
Promise.resolve().then(() => console.log("promise"));
console.log("end");
// start, end, promise, file done
fs.readFile is a libuv-backed macrotask (poll phase). Promise wins.requestAnimationFrame schedules a callback right *before* the next paint — better than setTimeout(0) for animations.| API | when it runs |
queueMicrotask(cb) | end of current task, before next macrotask |
Promise.resolve().then(cb) | same as queueMicrotask |
setTimeout(cb, 0) | next macrotask (+ ≥0/4ms delay) |
requestAnimationFrame(cb) | just before the next paint (~16ms cycle) |
setImmediate(cb) (Node) | check phase of current loop iteration |
requestAnimationFrame.queueMicrotask (or Promise.resolve().then).new Worker() runs JS on another thread.