Suggest an editImprove this articleRefine the answer for “Nested timers”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**A large number of nested `setTimeout` calls, where the callback schedules the next timer, will not overflow the stack, but they can flood the task queue, cause time drift, throttling and interface lag.** The stack does not grow, because each callback finishes before the next one runs: the new task simply goes into the macrotask queue. What you pay with is accuracy: the browser keeps a minimum interval of about 4 ms (up to 1000 ms and more in an inactive tab), the delays accumulate, and heavy work inside the callbacks stalls rendering. ```javascript function tick() { // work... setTimeout(tick, 0); // the next tick on the next turn of the Event Loop } setTimeout(tick, 0); ``` **Key point:** nested timers are safe for the stack but give no exact timing; compensate for drift when you need an even rhythm, and use `requestAnimationFrame` for animation.Shown above the full answer for quick recall.Answer (EN)Image**A large number of "nested" `setTimeout` calls, that is, when the callback schedules the next timer, will not overflow the call stack, but they can flood the task queue, cause time drift, throttling and interface lag.** The stack is cleared between calls; what you lose is timing accuracy. ## Theory ### TL;DR - There is no stack overflow: each callback finishes before the next one runs. - What grows instead is the macrotask queue, and the interface may start to lag. - The tick time drifts: delays accumulate from step to step. - Even with a delay of `0` a minimum interval applies, about 4 ms, and up to 1000 ms or more in an inactive tab. - All microtasks run in full between timers, so they stretch every step of the chain. - A recursive `setTimeout` beats `setInterval` when the callback sometimes runs longer than the interval. ### Quick example ```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, because `tick` returns right after scheduling the next call. But the ticks will run **under the minimum delay limit** and **with drift**. ### What exactly happens 1. **No stack overflow.** Each `setTimeout` puts the *next* task into the macrotask queue and finishes the current one, so 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 interface starts to lag. 3. **Drift, that is, accumulated delay.** The firing moment shifts: the callback runs **no earlier** than the given delay, and even **later** if the stack was busy. Over a long chain the shift accumulates. 4. **Minimum delay and throttling.** Even at `0` ms browsers apply a **minimum interval**, usually about 4 ms, and up to 1000 ms or more in an inactive tab. Nested timers with very small delays are clamped anyway. 5. **Microtasks between timers.** After every callback **all microtasks** run (`Promise.then`, `queueMicrotask`). If you create many microtasks inside a timer, they stretch each step of the chain further. 6. **Execution order.** Timers are taken from the queue by their ready time; when several become ready at once, the order can differ between engines, so it is better **not to rely** on a specific order. 7. **Memory.** If the callbacks close over large objects and the chain is long or endless, memory usage can **grow**, because those objects stay reachable for longer. ### How to do it better - **Compensate for drift** in periodic work: ```javascript const start = performance.now(); const step = 1000; // 1 second 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 animation use `requestAnimationFrame`, which is synchronised with repainting and drifts less. - For "every N ms" without overlaps prefer a **recursive `setTimeout`**, as above, over `setInterval`: that way calls never pile up when the callback occasionally runs longer than the interval. - Move heavy work into a **Web Worker** so the interface is not blocked. ### Recursive setTimeout vs setInterval | Property | Recursive `setTimeout` | `setInterval` | | --- | --- | --- | | The next run is scheduled | After the current callback finishes | Regardless of whether the callback finished | | Calls piling up | Impossible | Possible when the callback is slower than the interval | | Pause between runs | Guaranteed | Not guaranteed | | Variable delay | Easy, the value can be recomputed each time | No, the interval is fixed | | Stopping | `clearTimeout(id)` for the last timer | `clearInterval(id)` | ### Conclusion Nested `setTimeout` calls are a safe way to "break up" synchronous recursion, but: - there will be **no exact timing**; - with a large volume of tasks you get **lag and drift**; - the browser applies **minimum delays** and throttling. ### Common mistakes - **Expecting a stack overflow.** This is not recursion in the usual sense: the stack is empty between ticks, so no `Maximum call stack size exceeded` error appears, and the problem shows up later as lag. - **Scheduling several timers in each callback.** The chain branches and the number of queued tasks grows explosively. - **Counting on a delay of `0`.** Nested timers are clamped to roughly 4 ms, so a loop of 1000 steps takes at least a few seconds. - **Computing elapsed time as `step * tick count`.** Because of drift the real time is larger; read it from `performance.now()` instead. - **Never stopping the chain.** Without a flag or a stored id the timer runs forever, even after the screen that started it has been closed.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.