no-floating-timers
Require timer handles to be retained so they can be cleared during cleanup.
Rule catalog ID: R001
Targeted pattern scopeโ
This rule targets timer APIs that return handles with an explicit cleanup counterpart. It is intentionally conservative: the rule reports only calls where the returned handle is immediately discarded.
Matched patternsโ
The rule checks direct global calls to:
setTimeoutsetIntervalsetImmediaterequestAnimationFramerequestIdleCallback
It also checks the same methods when called through these global receivers:
globalThiswindowselfglobal
Detection boundariesโ
The rule does not prove that every retained handle is cleared. That broader lifetime analysis needs separate rules that understand cleanup blocks, effect callbacks, disposal protocols, and framework lifecycle APIs.
The rule also skips locally declared or imported direct timer identifiers, so a
project-local helper named setTimeout is not treated as the platform timer API.
What this rule reportsโ
The rule reports timer calls when the returned handle is used only as a bare
expression or deliberately discarded with void.
Why this rule existsโ
Floating timer handles make cleanup impossible. Intervals keep running, timeouts can fire after teardown, animation callbacks can touch detached state, and idle callbacks can outlive the component or request that scheduled them. Retaining the handle is the first enforceable step toward calling the matching cleanup API from the correct lifecycle boundary.
โ Incorrectโ
setInterval(poll, 1000);
globalThis.setTimeout(flush, 250);
void window.requestAnimationFrame(render);
โ Correctโ
const intervalId = setInterval(poll, 1000);
clearInterval(intervalId);
const timeoutId = globalThis.setTimeout(flush, 250);
clearTimeout(timeoutId);
return window.requestAnimationFrame(render);
timerRegistry.add(setTimeout(flush, 250));
Behavior and migration notesโ
Start by storing each reported handle in a variable, returning it to the caller,
or passing it to an existing timer registry. Then clear the handle from the
nearest lifecycle cleanup path, such as a finally block, component cleanup
callback, request teardown hook, or disposable object.
This rule does not autofix because choosing the correct cleanup location is a semantic decision. A mechanical fix that only introduces a variable would still leave the resource alive.
ESLint flat config exampleโ
import runtimeCleanup from "eslint-plugin-runtime-cleanup";
export default [
runtimeCleanup.configs.recommended,
];
When not to use itโ
Do not enable this rule for files that intentionally schedule process-lifetime timers and have no teardown path. Prefer a narrow ESLint disable comment at the call site with a reason when that pattern is deliberate.