In JavaScript, what is the difference between repeating work with setInterval(fn, 1000) and having fn schedule itself with setTimeout(fn, 1000) as its last step, and when does that difference matter?
answer
- measured from what, exactly
- cadence versus guaranteed gap
- slow callback eats the interval
- who decides the next delay
- no backlog of catch-up ticks
basics
~20 ssetInterval measures its period from tick to tick, so a slow callback eats into the gap and can run back-to-back. A self-rescheduling setTimeout guarantees a full delay after each run finishes, at the cost of a period that grows by the callback's own duration.
solid answer
~50 sWith `setInterval(fn, 1000)` the next occurrence is scheduled independently of how long `fn` takes, so the ticks target a fixed cadence: if `fn` burns 300 ms you get roughly 700 ms of idle before the next one, and if `fn` takes longer than the period the next invocation runs essentially immediately after it returns. With a self-rescheduling `setTimeout` you only arm the next run once the current one has finished, so you get a guaranteed gap of at least the delay between runs, and the effective period becomes delay plus the callback's own duration. Neither is exact — a timer delay is always a minimum, never an appointment. The practical reason to prefer the recursive `setTimeout` is control: at the point you schedule the next run you can pick the delay, back off after an error, correct for accumulated drift, or simply not schedule at all.
code
javascript · 14 linesfunction burn(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
let last = Date.now();
let n = 0;
const id = setInterval(() => {
const now = Date.now();
console.log('start-to-start', now - last);
last = now;
burn(30);
if (++n === 5) clearInterval(id);
}, 100);go deeper
Be able to write both forms correctly and say plainly that setInterval repeats on its own until cleared, while the setTimeout version only repeats because the callback schedules the next run.
Explain what each delay is measured from — tick to tick versus end to start — and work out the effective period when the callback itself takes a known amount of time.
Show the production reasoning: variable or unbounded callback duration, backoff on failure, and the fact that back-to-back invocations with no idle gap starve everything else on the thread.
Own the policy question — when repeating work should be self-rescheduling with backoff and jitter versus fixed cadence, and how that choice interacts with load on whatever the work talks to.
## The two shapes ```js // A: fixed cadence setInterval(fn, 1000); // B: self-rescheduling — a fixed gap between runs (function tick() { fn(); setTimeout(tick, 1000); })(); ``` They look interchangeable and are not. The difference is *what the delay is measured from*. ## What setInterval measures `setInterval` aims at a cadence. Each occurrence is scheduled without reference to how long your callback runs, so the observable spacing is tick start to tick start, not callback end to callback start. If the callback takes 30 ms out of a 100 ms interval, the ticks still land about 100 ms apart and the idle time between them shrinks to about 70 ms. Push the callback to 90 ms and you have 10 ms of headroom left; push it past 100 ms and there is none — the next invocation runs as soon as the current one returns, so the timer effectively becomes a tight loop of back-to-back work. One thing it does **not** do is build a backlog. Because the next occurrence is armed when the current one is dispatched, blocking the main thread for five seconds with a 100 ms interval does not queue fifty catch-up invocations waiting to stampede; you get one late tick and the schedule continues from there. That property is worth stating explicitly in an interview, because "it queues them all up" is a common wrong answer. ## What recursive setTimeout measures The self-rescheduling form arms the next run only after the current one has completed its synchronous work, so the delay is the **gap** between runs. The effective period is `delay + callback duration + lateness`, which means a job taking 300 ms with a 1000 ms delay actually repeats every ~1300 ms. You have traded cadence for a guaranteed breathing space: whatever else is happening, there is always at least `delay` of idle between two runs of your work. ## Neither one is exact Both are built on the same primitive, and in both cases the delay is a **minimum**. The timer only makes your callback eligible to run; it still has to wait for the current task and any queued work ahead of it. So each tick can be late, never early, and with `setInterval` that lateness carries forward into the next scheduling decision — which is exactly why counting `setInterval` ticks is not a way to measure elapsed time. ## Overlap: callbacks never interleave, but their work can JavaScript runs to completion on one thread, so two invocations of the same timer callback never execute simultaneously — the second cannot start until the first has returned. That guarantee is about the *synchronous* body only. If your callback kicks off asynchronous work and returns immediately, the timer will happily start another one while the first is still outstanding, and you can end up with many overlapping operations in flight. ## Why the recursive form usually wins in real code The deciding factor is rarely accuracy — it is that the recursive form gives you a decision point on every cycle: ```js let stopped = false; let delay = 1000; (function tick() { try { doWork(); delay = 1000; // recover after a good run } catch (err) { delay = Math.min(delay * 2, 30_000); // back off after a bad one } if (!stopped) setTimeout(tick, delay); })(); ``` With `setInterval` there is no such point: the delay is fixed at registration, the schedule keeps running whether the work succeeds or fails, and the only lever you have is cancelling the whole thing. Variable delays, exponential backoff, jitter, drift correction, and "stop after N failures" all fall out naturally from the recursive form. ## Cancellation ergonomics `setInterval` is easier to cancel: one handle, valid for the timer's whole life. The recursive form produces a new handle every cycle, so cancellation needs either a mutable variable holding the most recent handle or a boolean flag the next scheduling step consults. That is the one genuine ergonomic advantage of `setInterval` and worth acknowledging rather than pretending the recursive form is free. ## Choosing Reach for `setInterval` when the work is short, fixed-cost, purely synchronous, and cadence matters more than spacing — a UI heartbeat, a cheap re-render of a clock face. Reach for the self-rescheduling `setTimeout` when the callback duration is variable or unbounded, when it starts asynchronous work, when you want backoff or drift correction, or when running two cycles back-to-back with no gap would be harmful.
- If the callback consistently takes longer than the setInterval delay, do the missed ticks queue up and run in a burst afterwards?No. The next occurrence is armed when the current one is dispatched, so no backlog accumulates. What you actually see is invocations running back-to-back with essentially no idle time between them, and the schedule slipping later and later. If your work can exceed the period, that back-to-back behaviour is the argument for a self-rescheduling setTimeout instead.
- Cancelling a self-rescheduling setTimeout is more awkward than clearInterval. How do you handle it cleanly?Keep the latest handle in a single variable that the scheduling step overwrites, and clear that on stop — or set a boolean the next cycle consults before arming another timeout. Belt and braces is both: flip the flag so no new run is scheduled, and clearTimeout the pending handle so the one already armed does not fire.
- Which form would you choose for a UI heartbeat that just repaints a clock face, and why?setInterval is fine there: the work is short, synchronous, fixed-cost, and cadence is what the user perceives. The recursive form buys you nothing because there is no error path to back off from and no risk of the callback overrunning its period. Save the extra machinery for work whose duration you do not control.
saying these in an interview costs you the question
- Says setInterval waits for the callback to finish
- Claims missed ticks queue up and replay in a burst
- Treats either delay as an exact guarantee
- Thinks two callback invocations can overlap on the main thread
- Asserts recursive setTimeout is always more accurate