Skip to main content

no-floating-workers

Require worker handles to be retained so they can be terminated during cleanup.

Rule catalog ID: R004

Targeted pattern scopeโ€‹

This rule targets worker constructors whose active thread or shared worker connection should have an explicit lifecycle owner:

  • Worker
  • SharedWorker
  • window.Worker, self.Worker, and globalThis.Worker
  • window.SharedWorker, self.SharedWorker, and globalThis.SharedWorker
  • Worker imported from node:worker_threads or worker_threads

The rule reports worker instances that are immediately discarded or chained directly into a method call other than .terminate(). In both cases there is no remaining worker handle available for teardown.

What this rule reportsโ€‹

The rule reports:

  • standalone worker construction expressions such as new Worker("./worker.js");
  • voided worker construction such as void new SharedWorker("./worker.js");
  • immediate worker method chains such as new Worker("./worker.js").postMessage("start");
  • discarded Node.js worker_threads workers imported as Worker

It intentionally does not require same-function terminate() calls. Ownership can be transferred to a component instance, resource manager, returned disposable, using declaration, or longer-lived runtime owner.

Why this rule existsโ€‹

Workers run independently from the creating script. Browser Worker instances can be stopped with terminate(), and Node.js worker_threads.Worker instances expose terminate() and Symbol.asyncDispose. If the worker handle is not retained, later cleanup cannot reliably stop the work or release the owning runtime resource.

SharedWorker instances are intentionally shared across same-origin browsing contexts, but the creating page still needs a retained handle for its port and ownership bookkeeping. Discarding the handle makes that lifecycle implicit.

Incorrectโ€‹

new Worker("./worker.js");
void new SharedWorker("./shared-worker.js");
new Worker("./worker.js").postMessage("start");
import { Worker } from "node:worker_threads";

new Worker("./worker.js");

Correctโ€‹

const worker = new Worker("./worker.js");

worker.postMessage("start");
worker.terminate();
return new Worker("./worker.js");
registerWorker(new SharedWorker("./shared-worker.js"));
import { Worker } from "node:worker_threads";

await using worker = new Worker("./worker.js");

Behavior and migration notesโ€‹

Store worker instances in the lifecycle owner that will stop or dispose them. For UI components, that is usually the component setup scope or a disposable collection. For shared libraries, returning the worker or passing it to a dedicated resource manager keeps ownership explicit.

This rule does not autofix. Choosing the owner and termination point is a semantic decision, and inserting a variable without a matching terminate() or disposal 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 code that intentionally creates page-lifetime or process-lifetime workers and does not need an explicit cleanup owner. Prefer a narrow disable comment with a reason when a worker is meant to live for the whole runtime.

Further readingโ€‹