ESC

Type to search the knowledge base.

Clear All Timeouts

Track timer ids you created and clear them as a set. Monkey-patching the global is a test smell; a small TimerHost is the production shape.

intermediate5 min read
  • javascript
  • timers
  • interview

The brute-force way to clear all timeouts is a loop: for (let i = 1; i < 1_000_000; i++) clearTimeout(i). It “works” in a browser because ids are small integers. It also cancels every timer on the page: React effects, analytics beacons, a debounce you do not own, the next paint-adjacent callback a library queued. There is no public API to list pending timeouts. If you did not store the id, you do not get to clear it.

A second popular miss is monkey-patching window.setTimeout for the whole process. Tests then leak the patch, or they wipe timers that belong to the runner. Production code that does this is fighting every other bundle on the page.

Own a set of ids you created. Expose clearAll(). That is the whole problem.

Key takeaways

  • The runtime will not give you an iterator of pending timeouts. Track ids as you schedule.
  • clearAll() walks that set, calls clearTimeout, and empties it.
  • Drop the id when the callback runs, or the set leaks for the life of the page.
  • Patching the global setTimeout is a test smell. A TimerHost you inject is the production shape.

Own the ids, do not scrape the runtime

There is no getActiveTimeouts(). Node used to expose handle lists on the process; browsers never did, and scraping handles is not an interview answer you want. You record what you schedule.

function createTimerHost(api = globalThis) {
  const ids = new Set();

  function setTimeout(fn, delay, ...args) {
    const id = api.setTimeout((...cbArgs) => {
      ids.delete(id);
      fn(...cbArgs);
    }, delay, ...args);
    ids.add(id);
    return id;
  }

  function clearTimeout(id) {
    ids.delete(id);
    api.clearTimeout(id);
  }

  function clearAll() {
    for (const id of ids) api.clearTimeout(id);
    ids.clear();
  }

  function size() {
    return ids.size;
  }

  return { setTimeout, clearTimeout, clearAll, size };
}
const timers = createTimerHost();

const a = timers.setTimeout(() => {}, 1000);
const b = timers.setTimeout(() => {}, 2000);
timers.clearTimeout(a);
timers.clearAll(); // b is gone; a was already removed

Why wrap the callback: when the timer fires, the id is no longer pending. If you leave it in the set, clearAll() later calls clearTimeout on a dead id (harmless) and size() lies (not harmless in tests). Delete on fire and on explicit clear.

clearTimeout on an unknown id is a no-op in both browsers and Node. Your Set.delete should tolerate that too.

What a host is for

Pass the host into widgets instead of calling the global:

function DebouncedInput(host, { wait = 300, onValue }) {
  let lastId = null;

  function onInput(value) {
    if (lastId != null) host.clearTimeout(lastId);
    lastId = host.setTimeout(() => onValue(value), wait);
  }

  function dispose() {
    host.clearAll();
  }

  return { onInput, dispose };
}

Unmount calls dispose(). You do not need to remember each id at the call site if this component is the only client of that host. If several features share one host, prefer clearTimeout per feature or give each feature its own host. A process-wide clearAll is how you cancel someone else’s autosave.

Intervals: same set, setInterval / clearInterval, and do not delete the id on fire (intervals stay pending until cleared). Mixing timeout ids and interval ids in one set is fine if you store { id, kind } and dispatch to the right clearer. Colliding numeric ids across the two globals is not something browsers do in practice, but Node’s Timeout objects are distinct references, so a Set of handles is the right structure there.

Why monkey-patching the global is a test smell

const ids = new Set();
const realSet = globalThis.setTimeout;
const realClear = globalThis.clearTimeout;

globalThis.setTimeout = (fn, delay, ...args) => {
  const id = realSet(fn, delay, ...args);
  ids.add(id);
  return id;
};
globalThis.clearTimeout = (id) => {
  ids.delete(id);
  return realClear(id);
};

This captures timers from all callers: your code, React, the test framework, a delay helper inside an assertion library. clearAll in afterEach then races the runner. Forgetting to restore globalThis.setTimeout poisons later files. Fake-timer libraries already do this job, with restore and with a virtual clock. See fake timers for debounce and the fake setTimeout problem if they want you to implement the clock too.

If a take-home forces a patch, save the originals, wrap, and restore in afterEach. Still prefer injecting a host.

The for (i = 0; i <= lastId; i++) variant is worse. People do const last = setTimeout(() => {}, 0); to guess the high-water mark, then clear everything below it. You just scheduled a real timer to learn an id, then cancelled unrelated work. Do not scrape. Do not guess.

Footguns

  • Leaving fired ids in the set. Memory grows with every debounce keystroke for the session.
  • One global host for the whole app. clearAll on route change kills a toast timer in a portal.
  • Forgetting setInterval. clearTimeout(intervalId) happens to work in browsers; it is still the wrong method to teach, and it is not the Node story.
  • Clearing in the callback before fn. If fn wants to reschedule with the same wrapper, delete-then-run is correct; delete-after-run can clearAll a new id that reused nothing (ids do not reuse) but is easier to get wrong if you ever recycle ids.
  • Assuming ids are numbers. In Node they are Timeout objects. Store whatever setTimeout returned. Do not +id.

Uncleared timers keep closures alive: the callback scope, DOM nodes it closes over, fetch controllers. That is a real leak class in SPAs. See memory leaks in SPAs. Pair with abort on unmount for in-flight IO (cancelable async tasks).

Interview answer (out loud)

“I cannot list pending timeouts, so I wrap setTimeout, keep the returned ids in a Set, and clearAll walks that Set. I remove an id when the timer fires and when clearTimeout is called. I would not loop from 0 to a million, and I would not patch window.setTimeout in production. I inject a small TimerHost so dispose can drop only the timers this feature created. Intervals need the same tracking but they stay in the set until explicitly cleared.”

Further reading

Related guides