no-floating-child-processes
Require child process handles to be retained so they can be killed during cleanup.
Rule catalog ID: R005
Targeted pattern scopeโ
This rule targets asynchronous Node.js child process factories whose returned handle is needed for cleanup:
spawnexecexecFilefork
The rule recognizes those factories when they come from node:child_process or
child_process through named imports, default or namespace imports, CommonJS
require(...) module bindings, destructured require(...), or direct
require("node:child_process").spawn(...) calls.
The rule reports child process handles that are immediately discarded or chained
directly into a method call other than .kill() or .disconnect().
What this rule reportsโ
The rule reports:
- standalone child process calls such as
spawn("node", ["worker.js"]); - voided child process calls such as
void childProcess.exec("node worker.js"); - immediate method chains such as
childProcess.fork("./worker.js").on("exit", handleExit); - inline require calls such as
require("node:child_process").spawn("node", ["worker.js"]);
It intentionally does not require same-function kill() calls. Ownership can be
transferred to a supervisor, returned handle, disposable collection, or
process-lifetime owner.
Why this rule existsโ
Asynchronous child process factories return a ChildProcess handle. That handle
is the API surface for terminating the process, disconnecting IPC, inspecting
exit state, and wiring teardown. Discarding it leaves no explicit owner that can
clean up the subprocess when the surrounding runtime scope ends.
Incorrectโ
import { spawn } from "node:child_process";
spawn("node", ["worker.js"]);
import childProcess from "node:child_process";
void childProcess.exec("node worker.js");
import * as childProcess from "child_process";
childProcess.fork("./worker.js").on("exit", handleExit);
require("node:child_process").spawn("node", ["worker.js"]);
Correctโ
import { spawn } from "node:child_process";
const child = spawn("node", ["worker.js"]);
child.kill();
import { fork } from "node:child_process";
return fork("./worker.js");
import childProcess from "node:child_process";
registerChildProcess(childProcess.spawn("node", ["worker.js"]));
Behavior and migration notesโ
Store the ChildProcess handle in the owner that will terminate or supervise
the subprocess. For test helpers and task runners, that is usually a local
cleanup scope. For long-running services, it can be a process supervisor or
resource registry.
This rule does not autofix. Inserting a variable without a real shutdown path would only make ownership look explicit while preserving the leak.
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 that intentionally start detached, process-lifetime children and do not own their cleanup. Prefer a narrow disable comment with a reason for those launch points.