Skip to main content

Nested timers

In short: many "nested" setTimeout calls (where each callback schedules the next timer) do not overflow the stack, but they can clog the task queue, cause time drift, throttling, and interface lag.

What exactly happens

  1. No stack overflow. Each setTimeout schedules the next task in the macrotask queue and finishes the current one - the stack is cleared between calls.
  2. The macrotask queue grows. If each callback schedules several new timers or does heavy work, tasks start to pile up and the UI starts to lag.
  3. Drift (accumulating delay). The "tick" time shifts: the callback runs no earlier than the specified delay, and even later if the stack was busy. Over long chains, the shift accumulates.
  4. Minimum delay and throttling. Even at 0 ms, browsers enforce a minimum interval (usually about 4 ms; in an inactive tab, up to ~1000 ms or more). Nested timers with very small delays will still be "clamped".
  5. Microtasks between timers. After each callback, all microtasks run (Promise.then, queueMicrotask). If you create many microtasks inside a timer, they further lengthen each step of the chain.
  6. Execution order. Timers are pulled from the queue by readiness time; when several become ready "at the same time," the order may differ between engines, so it's best not to rely on a specific order.
  7. Memory. If callbacks close over large objects and the chain is long or infinite, memory usage can grow (objects stay reachable for longer).

Example of a "nested" chain

javascript
function tick() { // work... setTimeout(tick, 0); // the next tick - on the next turn of the Event Loop } setTimeout(tick, 0);
  • The stack does not grow, but the "ticks" will run with a minimum-delay limit and with drift.

How to do it better

  • Compensate for drift in periodic tasks:

    javascript
    const start = performance.now(); const step = 1000; // 1 sec let n = 0; function loop() { n++; // ...useful work... const nextAt = start + n * step; setTimeout(loop, Math.max(0, nextAt - performance.now())); } setTimeout(loop, step);
  • For animations use requestAnimationFrame - it is synchronized with repainting and drifts less.

  • For "every N ms" without overlaps, prefer a recursive setTimeout (as above) rather than setInterval - this way you avoid accumulation if the callback sometimes takes longer than the interval.

  • Move heavy work to a Web Worker so it does not block the UI.

Conclusion

Nested setTimeout calls are a safe way to "break up" synchronous recursion, but:

  • you will not get exact timing,
  • with a large number of tasks you will get lag and drift,
  • the browser will introduce minimum delays and throttling.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.