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.
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.jsInterpretation
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()