skip to content

What does setInterval return, and what happens if the code that started a repeating timer is torn down without ever calling clearInterval with that value?

level: juniorimportance: must knowfreq 62%

answer

  1. the handle exists only to cancel
  2. nothing stops it on its own
  3. closure stays reachable while active
  4. each call creates another timer
  5. clear before re-arming

basics

~20 s

setInterval returns a timer handle — a number in browsers, a Timeout object in Node — whose only use is to pass to clearInterval. Without that call the timer fires forever, keeping its callback and everything the callback closes over alive.

solid answer

~50 s

`setInterval` hands back an opaque handle identifying the timer inside the host's list of active timers: a positive integer in browsers, a `Timeout` object in Node. It carries no other information; its whole purpose is `clearInterval(handle)`. Nothing else stops a repeating timer — not the callback returning a value, not an exception thrown inside it, not the variable holding the handle going out of scope. That is why an uncleared interval is a real leak rather than just wasted CPU: the host keeps a strong reference to the callback, and the callback is usually a closure over the surrounding scope, so a whole object graph stays reachable. Meanwhile the callback keeps running against a world that no longer exists — writing to detached nodes, refetching for a screen nobody is looking at. Store the handle, clear it in teardown, and clear before re-arming so you never stack two timers.

code

javascript · 14 lines
javascript
let timerId = null;

function start(intervalMs) {
  stop(); // never stack a second timer on top of the first
  timerId = setInterval(() => console.log('poll', Date.now()), intervalMs);
}

function stop() {
  clearInterval(timerId); // a stale or null handle is a silent no-op
  timerId = null;
}

start(1000);
setTimeout(stop, 5000);

go deeper

for a junior

Know that setInterval returns a handle whose only purpose is clearInterval, and be able to write the start/stop pair that stores the handle and clears it on teardown.

for a middle

Explain why an uncleared interval pins the callback's whole closure scope in memory, and why calling the setup path twice produces two independent timers rather than reusing one.

for a senior

Show how you would diagnose it in production: sawtooth memory that never returns to baseline, requests or errors repeating at a round interval, and a heap snapshot where a timer callback is the retaining root.

for a principal

Argue for eliminating the class rather than fixing instances — ownership rules for timer handles, teardown enforced by the framework or a lifecycle helper, and a policy on whether repeating work should stop or back off when it starts failing.

## What the return value actually is `setInterval(callback, delay)` registers a repeating timer with the host environment and returns a handle. In a browser that handle is a positive integer, unique within that global (a `window` or a worker); in Node it is a `Timeout` object that also exposes `ref()`, `unref()` and `refresh()`. Either way, treat it as opaque: it is a key into the host's map of active timers, not something to compare, serialize, or do arithmetic on. ```js const id = setInterval(() => console.log('tick'), 1000); // ...later clearInterval(id); ``` ## Cancellation is the only exit A repeating timer has exactly one stopping condition: someone calls `clearInterval` with its handle. It is worth listing what does *not* stop it, because each is a real candidate answer from a weak interviewee: - **Returning a value from the callback** does nothing. There is no "return false to unsubscribe" convention here. - **Throwing inside the callback** ends only that one invocation. The exception surfaces as an uncaught error (`window.onerror` / `unhandledrejection` for async ones, `uncaughtException` in Node), and the next tick fires on schedule. A bug that throws throws once per period, forever. - **Losing the handle** — reassigning the variable, letting the enclosing function return — does nothing, because the host holds the timer, not your variable. Losing the handle just means you can no longer cancel it. `clearInterval` with an unknown, already-cleared, `null` or `undefined` handle is a silent no-op, never an error, so defensive clearing is safe and cheap. In browsers `clearTimeout` and `clearInterval` operate on the same map of active timers, so either one will clear either kind of timer — true, but relying on it reads as a mistake, so use the matching pair. ## Why an uncleared interval is a leak, not just a waste While a timer is active the host holds a strong reference to its callback function. Callbacks are almost always closures, so that reference transitively pins everything in the closed-over scope: the element you captured, the response payload you cached, the object holding the screen's state. Garbage collection cannot help — the object graph is genuinely reachable from a live root. The visible symptoms are the interesting part in an interview: memory that climbs in a sawtooth that never returns to baseline as the user navigates around; network requests for a view that closed minutes ago; errors like "cannot set property of null" repeating at a suspiciously round interval; and in a long-lived tab, a slow slide in responsiveness as more and more zombie timers share the main thread. ## Stacking: the other half of the bug Every call to `setInterval` creates an **independent** timer. Nothing deduplicates them. Setup code that runs again without a matching teardown therefore doubles the tick rate, and doubles again on the next run: ```js let timerId = null; function start(ms) { stop(); // clear before re-arming — never stack timerId = setInterval(poll, ms); } function stop() { clearInterval(timerId); // stale or null handle: silent no-op timerId = null; } ``` The giveaway in a bug report is "it polls twice per second, and after I open and close the panel a few times it polls constantly". Keeping the handle in exactly one place and clearing before re-arming removes the whole class. ## Host-specific consequence: process lifetime in Node In Node an active timer is a referenced handle that keeps the event loop alive, so a script with an uncleared interval never exits on its own. `handle.unref()` opts a specific timer out of that, letting the process exit while the timer is still pending. In a browser the equivalent consequence is simply that the timer lives as long as the document does. ## Practical checklist - Capture the handle in a single owning place; do not scatter copies. - Clear it in whatever teardown hook the surrounding code has, and clear before re-arming. - If the callback can throw, wrap its body in `try/catch` and decide deliberately whether a failure should cancel the timer — otherwise it repeats the failure indefinitely. - If the repeating work should stop on error, back off, or vary its delay, a self-rescheduling `setTimeout` gives you those decisions at the point where you choose the next delay; `setInterval` gives you only "on" and "off".

  • Does an exception thrown inside a setInterval callback stop the timer?
    No. Each invocation is a separate task, so the exception ends that invocation and surfaces as an uncaught error, while the timer stays registered and fires again next period. A callback that throws because the page it targets is gone will keep throwing forever. If a failure should end the schedule, catch it inside the callback and call `clearInterval` yourself.
  • What happens if you call clearInterval with a handle that was already cleared, or with undefined?
    Nothing — it is a silent no-op, not an error, because the handle simply is not in the host's map of active timers. That makes a defensive `clearInterval(timerId)` in a teardown path safe even when no timer is running, which is why the clear-then-re-arm pattern is cheap to apply everywhere.
  • In Node, why does a script with an active setInterval never exit?
    An active timer is a referenced handle, and Node keeps the event loop running while any referenced handle is pending. Calling `unref()` on the `Timeout` object returned by `setInterval` marks it as not keeping the process alive, so the script can exit with the timer still scheduled. Browsers have no equivalent — the timer simply lives as long as the document.

saying these in an interview costs you the question

  • Thinks the timer stops when the callback throws
  • Believes GC reclaims a timer once its handle is unreachable
  • Says returning false from the callback cancels it
  • Calls setInterval again on re-entry without clearing the old handle
  • Passes the callback function, not the handle, to clearInterval

context