Skip to content

Forgotten timers

setInterval registers a repeating handle — a GC root — until clearInterval runs. The callback’s closure captures every variable it references.

sequenceDiagram
  participant Timer as setInterval handle
  participant CB as Callback closure
  participant Data as Captured arrays
  Timer->>CB: invoke every N ms
  CB->>Data: push more data
  Note over Data: Grows forever if never cleared
function startPolling(fetchUser) {
setInterval(async () => {
const profile = await fetchUser(); // large object
history.push(profile); // outer scope array grows
}, 5000);
// interval id lost — cannot clear
}
let pollId = null;
const history = [];
const MAX = 50;
function startPolling(fetchUser) {
pollId = setInterval(async () => {
const profile = await fetchUser();
history.push(profile);
if (history.length > MAX) history.shift();
}, 5000);
}
function stopPolling() {
if (pollId !== null) {
clearInterval(pollId);
pollId = null;
}
}

In frameworks, return cleanup from useEffect (React) or onDestroy (Svelte).

Live demo: forgotten setInterval

Idle — each interval tick retains a growing closure scope.

— MBPeak: — MB