Skip to content

fix: route process.emit('exit'/'beforeExit') through user-visible emit#29196

Open
robobun wants to merge 13 commits intomainfrom
farm/a7624688/signal-exit-emit
Open

fix: route process.emit('exit'/'beforeExit') through user-visible emit#29196
robobun wants to merge 13 commits intomainfrom
farm/a7624688/signal-exit-emit

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Apr 11, 2026

Fixes #29194

Repro

import { onExit } from 'signal-exit';
onExit(function (code, signal) {
  console.log('process exited!');
}, { alwaysLast: true });
$ node test.mjs
process exited!
$ bun test.mjs
(no output)

Minimal demo without signal-exit, which monkey-patches process.emit:

const origEmit = process.emit;
const seen = [];
process.emit = function(ev, ...args) {
  seen.push(ev);
  return origEmit.call(this, ev, ...args);
};
process.on('exit', () => console.log(seen.join(',')));

Before: prints ``` (empty).
Node: prints newListener,beforeExit,exit.

Cause

Node dispatches the exit and beforeExit events by looking up emit on the process object at call time, so user overrides are honored. Bun was walking its internal EventEmitter listener list directly via process->wrapped().emit(...), bypassing any user-installed process.emit. That leaves signal-exit (and any other lib that hooks process.emit) silent during shutdown.

Call sites in src/bun.js/bindings/BunProcess.cpp:

  • dispatchExitInternal — called from natural-shutdown and process.exit(code)
  • Process__dispatchOnBeforeExit — called when the event loop drains

Fix

Extract a callProcessEmit helper that does a standard property get for "emit" on the process object, then invokes it with process as this. This is the same pattern Process_functionEmitHelper already uses for nextTick-routed events. exit and beforeExit now respect user overrides.

Behavior if user set process.emit to a non-function: fall back to the internal emitter so lifecycle events still fire. If the user emit throws, the exception is reported through reportUncaughtExceptionAtEventLoop.

The old hasEventListeners early-return in dispatchExitInternal is dropped — with user-visible dispatch, a patched emit should be called even without a registered listener (matches Node). _exiting = true is now set unconditionally, also matching Node.

Verification

Test in test/regression/issue/29194.test.ts spawns a child that monkey-patches process.emit, attaches an exit listener, and asserts the monkey-patch observed both beforeExit and exit. Covers:

  • natural shutdown dispatches exit through patched emit
  • natural shutdown dispatches beforeExit through patched emit
  • process.exit(code) dispatches exit through patched emit with correct code
  • signal-exit's wrapper pattern observes exit

Node dispatches the 'exit' and 'beforeExit' events by looking up the
'emit' property on the process object at call time, so user overrides
of process.emit (e.g. signal-exit's monkey-patch) are honored. Bun was
walking its internal EventEmitter listener list directly, bypassing any
user-installed emit, leaving libraries like signal-exit silent during
shutdown.

Look up 'emit' on the process object in dispatchExitInternal and
Process__dispatchOnBeforeExit and call it with process as 'this'.
If a user replaced emit with a non-function, fall back to the internal
emitter so lifecycle events still fire. Any thrown exception is
reported through the global object's uncaught-exception path.

Fixes #29194
@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Apr 11, 2026

Updated 1:24 PM PT - Apr 12th, 2026

❌ Your commit 3bdb8709 has 1 failures in Build #45365 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29196

That installs a local version of the PR into your bun-29196 executable, so you can run:

bun-29196 --bun

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Process lifecycle dispatch now invokes the runtime process.emit at call time (with exception reporting and an internal fallback) for "beforeExit" and "exit", and always drains the next-tick queue after "beforeExit". Three regression tests verify overridden process.emit receives shutdown events and expected exit behavior.

Changes

Cohort / File(s) Summary
Process Event Dispatch
src/bun.js/bindings/BunProcess.cpp
Added callProcessEmit(...) that retrieves and invokes process.emit at call time via a profiled JS call, reports exceptions with reportUncaughtExceptionAtEventLoop, and falls back to process->wrapped().emit(...) if not callable. Updated dispatchExitInternal(...) and Process__dispatchOnBeforeExit(...) to use this helper, removed prior listener pre-checks, and always drain m_nextTickQueue after "beforeExit".
Regression Tests
test/regression/issue/29194.test.ts
Added three child-process regression tests that monkey-patch process.emit before registering lifecycle listeners; each asserts the overridden process.emit is invoked for shutdown events and verifies stdout/stderr and exit codes.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: routing process.emit('exit'/'beforeExit') through user-visible emit instead of internal emitter.
Description check ✅ Passed The PR description comprehensively covers what the PR does, the root cause, the fix implementation, and verification through regression tests.
Linked Issues check ✅ Passed The PR fully addresses issue #29194 by making process.emit('exit') and process.emit('beforeExit') respect user overrides, allowing signal-exit and similar libraries to observe shutdown events.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the linked issue: BunProcess.cpp changes implement user-visible emit routing, and test file adds regression coverage for the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29194.test.ts`:
- Around line 73-74: Remove the flaky empty-stderr assertion by deleting the
expect(stderr).toBe("") line in the test (leave the exit code check
expect(exitCode).toBe(7) intact); this ensures the test no longer fails on
ASAN/CI spurious stderr output while still asserting the expected exitCode.
- Around line 107-108: Remove the empty-stderr assertion by deleting the
expect(stderr).toBe("") line in the test so ASAN warnings won't cause failures;
keep the existing expect(exitCode).toBe(0) check (i.e., locate the
expect(stderr).toBe("") statement near the exitCode assertion in the test and
remove it).
- Around line 41-42: Remove the brittle stderr assertion in the test so ASAN
warnings don't cause false failures: delete the expect(stderr).toBe("")
assertion in test/regression/issue/29194.test.ts (the line referencing stderr)
and rely solely on the exitCode assertion (expect(exitCode).toBe(0)) to detect
crashes/failures when spawning Bun subprocesses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 644e7d27-08bb-47d8-9ac8-bf58e9983bbd

📥 Commits

Reviewing files that changed from the base of the PR and between 6b9ee63 and 21d281d.

📒 Files selected for processing (2)
  • src/bun.js/bindings/BunProcess.cpp
  • test/regression/issue/29194.test.ts

Comment thread test/regression/issue/29194.test.ts Outdated
Comment thread test/regression/issue/29194.test.ts Outdated
Comment thread test/regression/issue/29194.test.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
test/regression/issue/29194.test.ts (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Drop empty-stderr assertions; they are flaky on ASAN CI.

expect(stderr).toBe("") can fail on ASAN due to known runtime warnings unrelated to regression correctness. Keep exit-code and stdout assertions as the signal instead.

🔧 Proposed fix
-  const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
-  expect(stderr).toBe("");
+  const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
@@
-  const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
-  expect(stderr).toBe("");
+  const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
@@
-  const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
-  expect(stderr).toBe("");
+  const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

Based on learnings: "In oven-sh/bun regression tests under test/regression/issue/*.test.ts, avoid asserting that spawned Bun subprocess stderr is empty... use exitCode as the primary crash/failure signal."

Also applies to: 65-65, 95-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/29194.test.ts` at line 37, Remove the flaky
empty-stderr assertions in test/regression/issue/29194.test.ts by deleting the
expect(stderr).toBe("") checks (currently at the assertion around line where
stderr is checked and the duplicates at the other two locations), and rely on
existing exitCode and stdout assertions as the test signal; update any related
comments or expectations in the same test function (the assertions surrounding
stderr, stdout, and exitCode variables) so the test no longer asserts stderr
equality to an empty string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29194.test.ts`:
- Around line 38-41: The test asserts process exitCode before validating stdout
which reduces diagnostic value; reorder the assertions so the
JSON.parse(stdout.trim()) and subsequent expect(seen).toContain("exit") and
expect(seen).toContain("beforeExit") run before expect(exitCode).toBe(0). Locate
the block using variables exitCode and stdout and the parsed seen array
(JSON.parse(stdout.trim())), move the stdout/seen assertions ahead of the
exitCode assertion, and apply the same reordering to the other occurrences
around the lines referencing exitCode and stdout (the similar blocks at the
other indicated positions).

---

Duplicate comments:
In `@test/regression/issue/29194.test.ts`:
- Line 37: Remove the flaky empty-stderr assertions in
test/regression/issue/29194.test.ts by deleting the expect(stderr).toBe("")
checks (currently at the assertion around line where stderr is checked and the
duplicates at the other two locations), and rely on existing exitCode and stdout
assertions as the test signal; update any related comments or expectations in
the same test function (the assertions surrounding stderr, stdout, and exitCode
variables) so the test no longer asserts stderr equality to an empty string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bb02b920-f2ee-4fb0-b794-46a387345d50

📥 Commits

Reviewing files that changed from the base of the PR and between 21d281d and c8ab619.

📒 Files selected for processing (1)
  • test/regression/issue/29194.test.ts

Comment thread test/regression/issue/29194.test.ts Outdated
ASAN CI lanes print warnings to stderr, so empty-stderr assertions
race. Using stdout payload + exit code as the crash signal instead,
with stdout checked first for clearer diagnostics.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29194.test.ts`:
- Around line 17-95: Add a regression test that covers the fallback path when
process.emit is non-callable by creating a new test (similar to the existing
ones) that sets process.emit = null (or a non-function), then registers an
"exit" handler and triggers a natural shutdown or explicit process.exit, and
asserts the handler runs and the process exits with the expected code; this
ensures the runtime path in callProcessEmit that falls back to the internal
emitter is exercised and guarded against regressions (reference symbols:
process.emit, callProcessEmit, and the existing test names like "process.emit
override is invoked for explicit process.exit(code)").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 777b10f5-0ee9-470b-ab96-4d9e59f16ee8

📥 Commits

Reviewing files that changed from the base of the PR and between c8ab619 and 999a10a.

📒 Files selected for processing (1)
  • test/regression/issue/29194.test.ts

Comment thread test/regression/issue/29194.test.ts
Comment thread src/bun.js/bindings/BunProcess.cpp Outdated
Comment thread test/regression/issue/29194.test.ts
…cessEmit

DECLARE_CATCH_SCOPE isn't used anywhere else in this codebase and was
causing CI cpp build lanes to fail. Rewrite callProcessEmit using the
exact pattern from constructStdioWriteStream:
- DECLARE_TOP_EXCEPTION_SCOPE
- JSC::profiledCall with ProfilingReason::API
- tryClearException(), not clearException()
- skip reporting termination exceptions, which JSC expects to propagate
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/bindings/BunProcess.cpp`:
- Around line 284-290: The fallback branch that calls process->wrapped().emit
when callData.type == JSC::CallData::Type::None can propagate a JS exception and
return early, skipping the same exception reporting and shutdown continuation
performed in the callable path; modify the fallback to mirror the callable path
by catching or checking for a pending exception after
process->wrapped().emit(...) and, if an exception exists, invoke
reportUncaughtExceptionAtEventLoop(...) with the exception and then continue to
call reallyExit(...) (or otherwise ensure shutdown proceeds) so both paths
behave identically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8b50ed92-5707-455c-b83e-87f1bbf4b972

📥 Commits

Reviewing files that changed from the base of the PR and between 999a10a and ce075c9.

📒 Files selected for processing (1)
  • src/bun.js/bindings/BunProcess.cpp

Comment thread src/bun.js/bindings/BunProcess.cpp Outdated
…Emit

When process.emit is replaced with a non-function, the fallback path
walks internal listeners directly. If one throws, mirror the callable
path and route the exception through reportUncaughtExceptionAtEventLoop
so process.exit(code) doesn't short-circuit reallyExit().
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — all prior review concerns (termination-exception handling, fallback-path exception reporting, assertion ordering) have been addressed.

Extended reasoning...

Overview

Adds a callProcessEmit helper in BunProcess.cpp that dispatches exit and beforeExit lifecycle events through the runtime-visible process.emit property (looked up at call time) rather than walking the internal EventEmitter listener list directly. This makes Bun match Node.js behavior for libraries like signal-exit that monkey-patch process.emit.

Security risks

None. This is a pure behavioral correctness fix for event dispatch ordering; no auth, crypto, or permissions code is touched.

Level of scrutiny

The change touches dispatchExitInternal and Process__dispatchOnBeforeExit — process lifecycle code that every Bun program runs through at shutdown. That warrants care, but the implementation is straightforward: it follows the existing Process_functionEmitHelper pattern and all exception-handling now correctly uses DECLARE_TOP_EXCEPTION_SCOPE, vm.isTerminationException() gating, and tryClearException(), matching every other call site in the file.

Other factors

All comments from the previous review round have been fully addressed across commits ce075c9, 999a10a, and f172e6b. The fallback (non-callable process.emit) branch now has identical exception-report-and-clear logic as the callable path. Regression tests follow repo conventions (stdout assertions before exitCode, no empty-stderr assertions, bunExe/bunEnv from harness).

…verrides

When process.emit isn't monkey-patched (the common case), dispatch
lifecycle events through the internal EventEmitter directly instead of
the JS-visible 'emit' binding. The binding goes through
EventEmitter::emitForBindings, which gates on scriptExecutionContext()
being alive — that is not guaranteed during natural shutdown, and the
gate was silently dropping 'exit'/'beforeExit' listeners (including
Node test harnesses' common.mustCall checks).

Check if 'emit' is an own property of process to detect user override.
Only the own-property case goes through the lookup-and-call path;
otherwise we call process->wrapped().emit() directly, matching the
pre-fix semantics for the default path.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — all prior review feedback has been addressed.

Extended reasoning...

Overview

The PR adds a callProcessEmit helper in BunProcess.cpp that routes exit and beforeExit lifecycle events through the user-visible process.emit at call time (checking for an own-property override), matching Node.js semantics. The test file adds three child-process regression tests covering natural shutdown, explicit process.exit(code), and the signal-exit wrapper pattern.

Security risks

None. This is a compatibility fix with no auth, crypto, or permissions involvement.

Level of scrutiny

The C++ changes touch process shutdown, which is critical but well-contained. The helper follows established exception-handling patterns in the file (DECLARE_TOP_EXCEPTION_SCOPE, tryClearException(), termination-exception skip), and the fallback path is symmetric with the callable path. All edge cases identified during review were resolved.

Other factors

All inline comments from prior review rounds are marked resolved. The three commits since initial submission (999a10a, ce075c9, f172e6b) address every issue raised: ASAN-safe test assertions, stdout-before-exitCode ordering, proper termination-exception handling, and fallback-branch exception reporting. No bugs were found by the automated bug hunting system.

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — all concerns from the previous review have been addressed.

Extended reasoning...

Overview

The PR adds a callProcessEmit helper in BunProcess.cpp that routes exit and beforeExit dispatch through the JS-visible process.emit own-property if one exists, matching Node.js's behavior. Two functions are updated: dispatchExitInternal and Process__dispatchOnBeforeExit. A regression test file covers natural shutdown, explicit process.exit(code), and the signal-exit wrapper pattern.

Security risks

No security-sensitive code paths (auth, crypto, permissions) are touched. The new dispatch code uses getOwnPropertySlot on the process object — a standard JSC lookup — and does not expose any new attack surface.

Level of scrutiny

This touches C++ process-lifecycle code, which warrants careful scrutiny. Multiple rounds of review identified and fixed: (1) use of DECLARE_CATCH_SCOPE (wrong pattern) → switched to DECLARE_TOP_EXCEPTION_SCOPE; (2) unconditional clearException() without checking vm.isTerminationException → fixed with the termination-guard pattern used everywhere else in the file; (3) fallback branch missing exception reporting → both callable and non-callable paths are now symmetric. The final code state correctly matches the established patterns in BunProcess.cpp and ZigGlobalObject.cpp.

Other factors

The bug hunting system found no issues. Tests use bunExe/bunEnv from harness, assert stdout before exitCode, and omit the ASAN-sensitive expect(stderr).toBe('') assertions. The nextTick-queue drain after beforeExit is now unconditional, matching Node semantics. All inline review comments are resolved.

getOwnPropertySlot may return true for entries in the prototype's
static property hash table (EventEmitter.prototype.emit), causing the
non-override path to take the JS-binding route through
emitForBindings — which drops listeners when scriptExecutionContext()
is being torn down during shutdown.

Use getDirect instead, which only reads instance storage. A value
there can only come from 'process.emit = fn', which is the exact
condition we want to detect for signal-exit-style overrides.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — all review concerns addressed.

Extended reasoning...

Overview

Modifies src/bun.js/bindings/BunProcess.cpp to add a callProcessEmit helper that routes exit/beforeExit lifecycle events through the user-visible process.emit property (via getDirect to detect own-property overrides only), with a fallback to the internal emitter. Adds three regression tests in test/regression/issue/29194.test.ts.

Security risks

None. The change is scoped to process lifecycle event dispatch and doesn't touch auth, crypto, or permission code.

Level of scrutiny

Medium — this is a process shutdown code path, but the logic is straightforward. The use of getDirect (rather than get) to detect own-property overrides only is the correct approach and consistent with how Node.js behaves (honoring monkey-patches without routing through the prototype chain).

Other factors

All prior review comments have been resolved: DECLARE_TOP_EXCEPTION_SCOPE + tryClearException() + termination-exception guard are in place; both branches of callProcessEmit share a single exception handler; test assertion ordering and ASAN-unsafe expect(stderr).toBe("") assertions were fixed. The pre-existing processIsExiting data race is not introduced by this PR.

Comment thread src/bun.js/bindings/BunProcess.cpp
robobun added 2 commits April 12, 2026 07:08
…rride

Earlier refactors removed the hasEventListeners guard and reordered
the putDirect(_exiting) call. Restore the exact original flow for the
no-user-override case — the only behavioral change should be an
additional user.emit dispatch path when process.emit is overridden.

Split the helper into userEmitOverride (pure lookup) and
callUserEmitOverride (call with exception handling), keeping the rest
of dispatchExitInternal byte-identical to pre-fix semantics.
Comment thread src/bun.js/bindings/BunProcess.cpp Outdated
robobun pushed a commit that referenced this pull request Apr 12, 2026
The hasEventListeners guard was previously gating the putDirect of
process._exiting, so when no exit listeners were registered, the flag
stayed false despite the process being in shutdown. Node sets it
unconditionally. Move the assignment before all early returns.

Reported by claude[bot] review on #29196.
The hasEventListeners guard was previously gating the putDirect of
process._exiting, so when no exit listeners were registered, the flag
stayed false despite the process being in shutdown. Node sets it
unconditionally. Move the assignment before all early returns.

Reported by claude[bot] review on #29196.
@robobun robobun force-pushed the farm/a7624688/signal-exit-emit branch from e3ccdf0 to 4015afb Compare April 12, 2026 10:38
Comment thread src/bun.js/bindings/BunProcess.cpp
beforeExit path uses the return value of emit() to decide whether to
drain the nextTick queue, matching Node. Previously the user-override
branch set fired=true unconditionally, causing the queue to drain
even when the patched emit returned false.

callUserEmitOverride now returns toBoolean of the JS result; the
beforeExit caller uses it for the drain decision. The exit caller
discards the value since it doesn't drain.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — all prior review concerns have been addressed.

Extended reasoning...

Overview

This PR routes process.emit('exit') and process.emit('beforeExit') through the user-visible emit property (using getDirect) rather than directly invoking the internal EventEmitter listener list. It adds a userEmitOverride helper and a callUserEmitOverride helper in BunProcess.cpp, plus three regression tests in test/regression/issue/29194.test.ts.

Security risks

None. The change only affects how lifecycle events are dispatched during process shutdown and does not touch auth, crypto, or permissions.

Level of scrutiny

Moderate — BunProcess.cpp is runtime-critical, but the change is well-scoped to two call sites (dispatchExitInternal and Process__dispatchOnBeforeExit) with clear Node.js compatibility semantics.

Other factors

All four issues identified in earlier review rounds were fixed:

  • Exception scope type and termination-exception guard (ce075c9)
  • Fallback path exception reporting symmetry (f172e6b)
  • _exiting set unconditionally before any early return (4015afb)
  • fired correctly derived from the override return value (23538f9)

The bug hunting system found no additional issues. Tests cover the three primary scenarios (natural shutdown, explicit process.exit(code), and signal-exit-style wrapper).

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — all prior review concerns addressed.

Extended reasoning...

Overview

This PR routes process.emit('exit') and process.emit('beforeExit') through the user-visible process.emit property at call time rather than walking the internal EventEmitter listener list directly. Two files changed: src/bun.js/bindings/BunProcess.cpp (new userEmitOverride/callUserEmitOverride helpers + updated dispatchExitInternal and Process__dispatchOnBeforeExit) and a new regression test file.

Security risks

No security-sensitive code paths. The change is purely about Node.js compatibility for event dispatch during shutdown — no auth, crypto, or permissions involvement.

Level of scrutiny

The change touches production-critical process lifecycle code (shutdown events), which warrants careful review. However, the implementation has gone through multiple rounds of iterative fixing and all substantive issues have been resolved: exception handling now uses DECLARE_TOP_EXCEPTION_SCOPE + tryClearException() with a termination-exception skip; the fallback (non-callable user emit) path has symmetric exception reporting; _exiting is set unconditionally before any early return; and callUserEmitOverride returns the JS truthy result so the beforeExit drain decision matches Node semantics.

Other factors

All inline comments from prior review rounds are resolved. The three regression tests cover natural shutdown, explicit process.exit(code), and the signal-exit monkey-patch pattern, with assertions in the correct order (stdout before exitCode). The remaining pre-existing data-race on the static bool is acknowledged out of scope and predates this PR. No new bugs found by the automated hunting system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

signal-exit onExit callback not triggered in Bun during process shutdown

1 participant