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.
- 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, callsclearTimeout, and empties it.- Drop the id when the callback runs, or the set leaks for the life of the page.
- Patching the global
setTimeoutis a test smell. ATimerHostyou 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.
clearAllon 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. Iffnwants to reschedule with the same wrapper, delete-then-run is correct; delete-after-run canclearAlla 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
Timeoutobjects. Store whateversetTimeoutreturned. 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 on this site
- Implement a fake setTimeout
- Cancelable async tasks
- Debounce implementation
- The event loop
- Memory leaks in SPAs
- Fake timers for debounce
Related guides
- Implement a Fake setTimeoutA timer queue you can run in tests: schedule, clear, sort by due time, and flush without a real clock. Why a busy while-loop is a bad host.
- Analytics SDK Event QueueQueue analytics events, send them in series, retry the ones that fail, and flush on page hide without dropping the last click.
- Arrays That Emit Change EventsWrap push, pop, and splice so subscribers hear adds and removes. Proxy vs subclass, and why mutating via index assignment still slips through.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.