Tips

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.

Watch on Facebook

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

  1. A task occupies the call stack for 26ms; nothing can interrupt it.
  2. That task creates a 0ms timer (into the macrotask queue) and 3 promises (into the microtask queue).
  3. The stack empties → the event loop drains all microtasks first, including any extra thens they create.
  4. 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

ComponentHoldsRuns when
Call stackCurrently running codeImmediately, until done
Microtask queuePromise.then, queueMicrotaskAfter the stack empties, drained fully before the next task
Macrotask queuesetTimeout, UI events, I/OOne 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 setTimeout and 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/await when you need a clear order.
  • Break up heavy work: split it into chunks, use requestIdleCallback or Web Workers.
  • Watch out for microtask loops: promises that keep spawning new promises.
  • Node.js adds process.nextTick and 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 requestAnimationFrame or splitting work per frame.

Sources & references

  1. The event loop — MDN Web Docs