clearAllTimers
intermediateTrack and clear every active timeout and interval — a utility question about monkey-patching globals safely.
Implement setInterval via recursive setTimeout in JavaScript, understand overlap vs drift trade-offs, and build the drift-corrected scheduler clocks and pollers need.
Implement
mySetInterval(callback, interval)using onlysetTimeout, with a workingclear— then explain what your version does differently from nativesetInterval, because the differences are the question.
This is a scheduling-semantics question dressed as a polyfill. There are three possible contracts, and strong candidates name all three:
setInterval — fires on a fixed schedule regardless of how long the callback takes; if the callback is slower than the interval, executions queue up and can run back-to-back (and in bad cases, effectively overlap via queued timers).setTimeout — schedules the next run after the current one finishes: guaranteed spacing, no pile-up, but each cycle's duration = interval + callback time, so the schedule drifts.setTimeout — recomputes each delay against an absolute schedule: no pile-up and no long-term drift. What clocks and pollers actually want.function mySetInterval(callback, interval, ...args) {
let timerId = null;
let cancelled = false;
function schedule() {
timerId = setTimeout(() => {
callback(...args);
if (!cancelled) schedule(); // re-arm AFTER the callback completes
}, interval);
}
schedule();
return {
clear() {
cancelled = true; // covers "clear() called from inside the callback"
clearTimeout(timerId); // covers the currently pending timer
},
};
}Why both cancellation mechanisms: timerId changes every cycle. If clear() is called while the callback is executing, the pending timer id is already stale and the recursive schedule() hasn't run yet — the flag is what stops the re-arm. Interviewers test precisely this by calling clear from inside the callback.
interval = 1000, callback takes ~40ms
native setInterval: fires at 1000, 2000, 3000, ... (fixed schedule)
recursive setTimeout: fires at 1000, 2040, 3080, 4120 ... (interval + work, compounding)After an hour, contract 2 is ~2.4 minutes behind. Fine for "poll every second-ish"; disqualifying for a clock display or anything aligned to wall time.
Anchor to an absolute schedule and compute each delay as "time until the next planned tick":
function preciseInterval(callback, interval) {
let timerId = null;
let cancelled = false;
const start = performance.now();
let tick = 0;
function schedule() {
tick += 1;
const nextAt = start + tick * interval; // absolute target, no accumulation
const delay = Math.max(0, nextAt - performance.now());
timerId = setTimeout(() => {
callback();
if (!cancelled) schedule();
}, delay);
}
schedule();
return { clear() { cancelled = true; clearTimeout(timerId); } };
}Each delay self-corrects: run 30ms late this cycle and the next delay is 30ms shorter. Error stays bounded instead of compounding — the same trick game loops and metronomes use. One decision to state out loud: if the callback takes longer than the interval, Math.max(0, ...) makes late ticks fire immediately back-to-back (catch-up). The alternative — skipping missed ticks (tick = Math.ceil((now - start) / interval)) — is right for UI clocks, wrong for counters. Which one you want is a requirements question; noticing that it's a fork is the senior signal.
clear from within the callback — the flag + stale-id interplay; the planted test.setInterval keeps firing. Wrap in try/finally with the re-arm in finally if the loop must survive errors — and decide, don't default.interval of 0 or negative — stays async (timer clamp), never a synchronous spin; and nested-timer clamping (~4ms after 5 levels) puts a floor under tiny intervals.performance.now() recovers correctly on return, a naive one doesn't. Pair with visibilitychange for anything user-visible.clear that only does clearTimeout(timerId) — the re-arm race leaves the interval running.interval <= 0 — and in the widely circulated "enhanced" variant, also never scheduling the loop, so the "interval" runs exactly once. Two bugs in four lines.setTimeout "fixes drift" — it fixes overlap; it makes drift worse. The corrected version fixes drift. Keeping the two claims straight is half the question....args (native setInterval(fn, ms, a, b) does).Date.now() for the schedule anchor — wall-clock adjustments corrupt the whole schedule; performance.now() is monotonic.setInterval?" — short-lived, cheap callbacks where the fixed cadence matters and pile-up can't realistically happen; it's also the only one visible to some devtools/test fakes without extra wiring.document.visibilityState when computing the next delay — this is why pollers use contract 2/3, never native.setTimeout + await callback() gives between-completions; between-starts needs the absolute schedule. Same fork as catch-up-vs-skip, now with promises (series execution thinking).performance.now: simulate a 30ms-late tick and assert the next delay shrank by 30ms — testing the invariant, not timestamps.Track and clear every active timeout and interval — a utility question about monkey-patching globals safely.
Rebuild setTimeout on top of requestAnimationFrame to show you understand timer scheduling and drift.
Implement throttle and know exactly when to reach for it instead of debounce (scroll, resize, mousemove).
Implement debounce from scratch, with leading/trailing options and the classic search-input use case.