Skip to content

fix(ext/node): process.exit() in worker immediately halts execution#32169

Merged
bartlomieju merged 4 commits intodenoland:mainfrom
bartlomieju:fix/worker-exit-halt
Feb 15, 2026
Merged

fix(ext/node): process.exit() in worker immediately halts execution#32169
bartlomieju merged 4 commits intodenoland:mainfrom
bartlomieju:fix/worker-exit-halt

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

Summary

In Node.js, process.exit() inside a worker immediately stops all JS execution at the C++ level. In Deno, it called workerClose() which triggered V8's terminate_execution(), but JS continued running because V8 only checks the termination flag at function entries and loop back-edges — not during function returns.

Adds an infinite loop after process.reallyExit() that gives V8 a loop back-edge to detect the pending termination and throw an uncatchable TerminationException. On the main thread the loop is unreachable since reallyExit() calls std::process::exit().

Enables 2 new node_compat tests:

  • test-worker-voluntarily-exit-followed-by-addition.js
  • test-worker-voluntarily-exit-followed-by-throw.js

Test plan

  • cargo test --test node_compat parallel -- --filter "test-worker-voluntarily-exit" — both pass
  • cargo test --test node_compat parallel -- --filter "test-worker-terminate" — no regressions
  • cargo test --test node_compat parallel -- --filter "test-worker-exit" — no regressions

🤖 Generated with Claude Code

In Node.js, process.exit() inside a worker immediately stops all JS
execution at the C++ level. In Deno, it called workerClose() which
triggered V8's terminate_execution(), but JS continued running because
V8 only checks the termination flag at function entries and loop
back-edges, not during function returns.

Fix by adding an infinite loop after process.reallyExit() that gives
V8 a loop back-edge to detect the pending termination and throw an
uncatchable TerminationException. On the main thread the loop is
unreachable since reallyExit() calls std::process::exit().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju
Copy link
Copy Markdown
Member Author

The fix seems a bit bizarre, but does work

Comment on lines +132 to +142
// In a worker, reallyExit() returns because Deno.exit() calls workerClose()
// instead of std::process::exit(). But workerClose() already called V8's
// terminate_execution(). Spinning here gives V8 a loop back-edge to detect
// the pending termination and throw an uncatchable TerminationException,
// matching Node.js behavior where process.exit() immediately halts all JS.
// On the main thread reallyExit() normally never returns, but users can
// override it (test-process-really-exit.js), so only spin in workers.
if (internals.__isWorkerThread) {
// deno-lint-ignore no-empty
for (;;) {}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume there is a stack guard in V8 for this so just a function call should be enough to trigger this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that with Claude I got this:

The tests fail — nop() alone isn't sufficient. The issue is that in Deno, process.reallyExit is a JS function (not a C++ binding like in Node.js), so V8's
stack guard spoofing from terminate_execution() may not persist through the multiple JS return frames between the op and the nop() call. The for (;;) {}
loop is needed because it provides a loop back-edge where V8 reliably checks for termination.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, makes sense

@bartlomieju bartlomieju merged commit 1951149 into denoland:main Feb 15, 2026
86 checks passed
@bartlomieju bartlomieju deleted the fix/worker-exit-halt branch February 15, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants