setTimeout(fn, 0) runs 50ms later? The event loop explains why
TL;DR
JavaScript has a single main thread. setTimeout(fn, 0) just queues the callback as a macrotask; it must wait for the current code to finish and for every microtask (Promise.then) to drain first. So “0ms” can easily mean tens of milliseconds.
You write setTimeout(fn, 0) expecting the function to run "immediately". But measuring shows it runs 50ms later. The fault isn't the browser or your machine; it's how JavaScript's event loop works. JavaScript has a single main thread: setTimeout(fn, 0) only places the callback in the macrotask queue, where it must wait for the current code to finish and for all microtasks (Promise.then) to be processed first.
Execution order
- A task occupies the call stack for 26ms; nothing can interrupt it.
- That task creates a 0ms timer (into the macrotask queue) and 3 promises (into the microtask queue).
- The stack empties → the event loop drains all microtasks first, including any extra
thens they create. - Only then is it the macrotasks' turn: by now the "0ms" timer has waited 50ms.
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// sync → promise → timeout
A mental model
| Component | Holds | Runs when |
|---|---|---|
| Call stack | Currently running code | Immediately, until done |
| Microtask queue | Promise.then, queueMicrotask | After the stack empties, drained fully before the next task |
| Macrotask queue | setTimeout, UI events, I/O | One task per event loop iteration |
So "0ms" means "no earlier than now", not "right now". Browsers may also enforce a minimum delay for nested timers.
Practical consequences
- Long tasks block everything: the UI stops responding, timers are late, animations stutter.
- Unexpected ordering when mixing
setTimeoutand Promises. - Endless microtasks can starve macrotasks and freeze the UI.
How to apply it
- Don't use
setTimeout(0)for timing or to assume exact ordering; use Promises/awaitwhen you need a clear order. - Break up heavy work: split it into chunks, use
requestIdleCallbackor Web Workers. - Watch out for microtask loops: promises that keep spawning new promises.
- Node.js adds
process.nextTickand its own phases, but the microtask-first principle still holds.
Debugging tips
- Use the DevTools Performance tab to see long tasks.
- Measure with
performance.now()instead of assuming. - When you need to "yield" so the browser can repaint, consider
requestAnimationFrameor splitting work per frame.
Sources & references
- The event loop — MDN Web Docs