process.exit(): throw `process.reallyExit is not a function`, pass process as this
Problem
process.exit()callsprocess.reallyExit(code), as Node.js does. Whenprocess.reallyExitis not callable, Bun throws aTypeErrorwith an empty message. Node.js throwsTypeError: process.reallyExit is not a function.- A build with assertions aborts:
ASSERTION FAILED: !message.isEmpty()atvendor/WebKit/Source/JavaScriptCore/runtime/Error.cpp(71)inJSC::createTypeError. - The cause is
JSC::call(globalObject, reallyExitVal, args, ""_s)atsrc/jsc/bindings/BunProcess.cpp:913. The last argument is the message for a value that is not callable. This overload also passes the function as its ownthis. Node.js passesprocess.
Fix
- The call passes
processasthisand the messageprocess.reallyExit is not a function. - Correct because Node.js runs
process.reallyExit(process.exitCode || 0)in JavaScript. That is a method call onprocess, and V8 gives that message. - Verified: two new tests in
test/js/node/process/process.test.js. Both fail on release 1.4.3-canary.1. Also ran the otherprocess.exit()tests and 12 exit tests fromtest/js/node/test/parallel/.
Background
process.reallyExitis the raw exit of Node.js: it emits no'exit'event. Libraries such as signal-exit replace it.JSC::callis the JavaScriptCore helper that calls a JS function from C++.- No other design applies: the two wrong values are arguments of one call. No other
JSC::callinsrc/has an empty message.
Downsides
- A function that replaces
process.reallyExitnow seesthis === processwhenprocess.exit()calls it. Before,thiswas the function itself. - Size: +53 bytes in the
BunProcess.cppobject (-O3, no LTO): 37 for the message, 16 of code. Per call: no new allocation, branch, or syscall.
Notes
Repro:
process.reallyExit = "str";
process.exit(0);
| stderr | exit code | |
|---|---|---|
| Bun 1.4.3-canary.1+367d939d9 (release) | TypeError: | 1 |
| Bun 1.4.3-debug+367d939d9 | ASSERTION FAILED: !message.isEmpty() | 134 |
| this branch (debug) | TypeError: process.reallyExit is not a function | 1 |
| Node.js v26.3.0 | TypeError: process.reallyExit is not a function | 0 |
The same result for each value tried: "str", undefined, null, 1, {}, Symbol("x"), and delete process.reallyExit.
The this value, with process.reallyExit = function () { console.log(this === process) } and process.exit(3): false before, true on this branch and on Node.js v26.3.0.
Not changed by this PR: when nothing catches the TypeError, Node.js exits with the code given to process.exit() (0 above, 5 for process.exit(5)). Bun exits 1. A replaced process.reallyExit that throws shows the same difference. For an entry point that throws, uncaught_exception (src/jsc/VirtualMachine.rs:2161) and exit_with_unhandled_note (src/runtime/cli/run_command.rs:1638) both set exit code 1. Related to #42032, which changes the first place for an 'exit' listener that throws. The new test catches the error in the child, so it does not depend on that exit code.
History: #16026 added the call with the comment // process.reallyExit(exitCode);.
Search for other call sites: JSC::call, call, profiledCall and construct with an empty message literal in src/ and packages/. BunProcess.cpp:913 is the only one.
Size measurement: compiled BunProcess.cpp from origin/main and from this branch with the release flags of the build (-O3 -march=nehalem), without -flto=thin so that the object is native code. size -A: .text of Bun::Process_functionExit 712 to 728 bytes, .rodata.str1.1 10594 to 10631 bytes, .data and .bss unchanged.
On a debug build without the fix, the first new test fails: the child exits with code 134 and the assertion is in its stderr.
Tests run on the debug build of this branch:
test/js/node/process/process.test.js -t "process.exit\(\)": 6 pass.test/js/node/process/process.test.js, whole file: 179 pass, 5 skip, 3 fail.processfails because the container sets noUSER.signal > simple case worksandsignal > process.emit will call signal eventstime out at 5 s in the whole-file run, with and without the fix. Each passes in 2.5 s when it runs alone.test/js/node/test/parallel/:test-process-exit-code-validation.js,test-process-exit-code.js,test-process-exit-from-before-exit.js,test-process-exit-handler.js,test-process-exit-recursive.js,test-process-exit.js,test-process-really-exit.js,test-worker-exit-code.js,test-worker-nested-on-process-exit.js,test-worker-on-process-exit.js,test-worker-process-exit-async-module.js,test-crypto-op-during-process-exit.js: all exit 0.
Not run: macOS, Windows. The changed line has no platform-specific code.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/process/process.test.js