no-floating-file-watchers
Require Node.js file watcher handles to be retained so they can be closed.
Rule catalog ID: R010
Targeted pattern scopeโ
This rule targets Node.js fs.watch() calls from fs and node:fs:
watchfs.watchrequire("fs").watchrequire("node:fs").watch
The rule reports watcher handles that are immediately discarded or chained
directly into a method call other than .close(). It does not target
node:fs/promises async iterator watchers because those use different
ownership and cancellation patterns.
What this rule reportsโ
The rule reports:
- standalone watcher creation such as
watch("src", onChange); - voided watcher creation such as
void fs.watch("src", onChange); - discarded CommonJS namespace or destructured
watchcalls - immediate event registration on an unowned watcher such as
fs.watch("src").on("error", onError);
It intentionally does not require same-function close() calls. Watcher
ownership can be transferred by returning the watcher or passing it to a
dedicated lifecycle manager.
Why this rule existsโ
fs.watch() returns an FSWatcher. Active watchers can keep the Node.js event
loop alive and continue receiving file-system events until they are closed. If
the watcher handle is discarded, later cleanup cannot reliably call .close()
or coordinate shutdown behavior.
Incorrectโ
import { watch } from "node:fs";
watch("src", onChange);
import * as fs from "node:fs";
void fs.watch("src", onChange);
import * as fs from "node:fs";
fs.watch("src", onChange).on("error", onError);
Correctโ
import { watch } from "node:fs";
const watcher = watch("src", onChange);
watcher.close();
import * as fs from "node:fs";
return fs.watch("src", onChange);
registerWatcher(fs.watch("src", onChange));
Behavior and migration notesโ
Keep the watcher handle in the owner that will close it during teardown. For development tools, that is usually the process-level watcher manager. For library code, returning the watcher or passing it to a lifecycle manager makes the ownership contract explicit.
This rule does not autofix. Introducing a variable without a matching close path would hide the lifecycle problem instead of fixing it.
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 scripts where a watcher intentionally lives until process exit and no explicit shutdown path is needed. Prefer a narrow disable comment with a reason for those cases.