Implement a polyfill for setInterval
Implement setInterval using recursive setTimeout: schedule a timeout that runs the callback then reschedules itself. This is actually BETTER than native setInterval — it guarantees a full gap between executions and never stacks callbacks. Return a handle with a clear method.
Polyfilling setInterval with recursive setTimeout is a classic — and the result is arguably better than the native version.
The implementation
function customSetInterval(callback, delay, ...args) {
let timerId;
let cancelled = false;
function tick() {
if (cancelled) return;
callback(...args);
if (!cancelled) timerId = setTimeout(tick, delay); // reschedule AFTER running
}
timerId = setTimeout(tick, delay);
// return a handle to clear it
return {
clear() {
cancelled = true;
clearTimeout(timerId);
},
};
}
// usage
const interval = customSetInterval(() => console.log("tick"), 1000);
// interval.clear();The idea: setTimeout runs the callback, and only then schedules the next timeout. It recurses forever until cancelled.
Why this is better than native setInterval
Native setInterval schedules executions on a fixed cadence regardless of how long the callback takes. If the callback takes longer than the interval (or the tab is throttled/busy), executions queue up and fire back-to-back with no gap — "callback stacking."
Recursive setTimeout schedules the next run after the current one finishes, so there's always a full delay gap between executions. No stacking, no overlap. This matters a lot for things like polling an API — you never want a second request firing before the first returns.
Things to get right
- Cancellation — return a handle (or id) with a way to stop it; set a
cancelledflag andclearTimeout, so a pending timeout doesn't fire afterclear(). - Forward arguments —
...argspassed through to the callback, like the real API. - Re-check
cancelledafter the callback runs — the callback itself might have cleared the interval.
The framing
"Recursive setTimeout: a tick function that runs the callback, then schedules itself again with setTimeout(tick, delay), repeating until cancelled. I return a handle with a clear() that sets a cancelled flag and clears the pending timeout. The interesting part is this is better than native setInterval — native fires on a fixed cadence and stacks callbacks if one runs long, while recursive setTimeout guarantees a full gap between executions, which is exactly what you want for polling."
Follow-up questions
- •Why does recursive setTimeout avoid callback stacking?
- •How would you handle the callback throwing an error?
- •What happens to timers in a backgrounded browser tab?
- •How would you implement a self-correcting interval that accounts for drift?
Common mistakes
- •Rescheduling before running the callback instead of after.
- •No cancellation mechanism, or only clearing the timeout without a cancelled flag.
- •Not forwarding extra arguments to the callback.
- •Claiming it's identical to setInterval — it's actually more stable.
Performance considerations
- •Recursive setTimeout prevents callback pile-up under load or in throttled tabs — far safer for polling than native setInterval. Neither is precise; drift accumulates, so for timing-critical work compute elapsed time rather than counting ticks.
Edge cases
- •Callback runs longer than the delay.
- •Callback throws — should it stop or continue?
- •clear() called from inside the callback.
- •Browser tab throttling timers when backgrounded.
Real-world examples
- •Polling an API every N seconds without overlapping requests.
- •Animation/game loops and countdown timers that must not stack.