← Debugging Guides

阅读中文版本

Node.js — worker thread does not exit or terminate

Fix worker threads that never terminate by closing message channels, avoiding active handles and calling terminate as a fallback.

advanceddebuggingnodeworkers

Symptoms

  • The process does not exit after work finishes
  • worker.terminate resolves but the process still hangs
  • An open handle keeps the event loop alive
  • CPU stays busy after the task should be done

Possible causes

  • A worker keeps an active handle such as a timer or socket open
  • The message channel between worker and parent is never closed
  • The worker waits on a promise that never settles
  • A pool worker stays alive by design but the parent does not shut it down

Diagnosis steps

List active handles

node -e "process._getActiveHandles().forEach(h=>console.log(h.constructor.name))"

Interpretation

Handles such as Timeout or Socket keep the loop alive. The names reveal what was not closed.

Ask why the process stays up

node --trace-exit app.js

Interpretation

The trace shows what keeps the loop alive when no explicit reason is logged.

Terminate as a fallback

await worker.terminate()

Interpretation

Termination stops the thread forcefully and returns an exit code, useful when graceful shutdown does not happen.

Root cause

The worker or parent still holds an active handle or an unresolved promise, so the event loop never becomes idle and the process cannot exit naturally.

Fix

Close timers, sockets and message ports when work completes, ensure workers settle their promises or are terminated, and call worker.terminate as a bounded fallback during shutdown.

Verify the fix

Run the workload and confirm the process exits within a few seconds, and confirm active handles are empty after completion.

Symptom walkthrough

A program finishes its work but the process never exits; the shell prompt does not return and CI jobs time out. Listing active handles shows a timer, socket or message port still open, which keeps the event loop alive. Terminating the worker may resolve its promise yet the parent still hangs on its own handles, so both sides must be considered.

Fix in practice

Close every resource when work completes: clear timers, close sockets and ports, and let worker promises settle. In a pool, shut the pool down explicitly. Keep worker.terminate as a bounded fallback so shutdown cannot hang indefinitely even if a worker misbehaves.

Verification

Rerun and confirm the process exits within a few seconds, then check that no active handles remain after completion. A clean exit without leftover handles proves the shutdown path is correct.

Diagnostic commands

Check the behavior with commands like these:

node -e "process._getActiveHandles().forEach(h=>console.log(h.constructor.name))"
node --trace-exit app.js
await worker.terminate()