Skip to content

napi: clear pending exceptions between finalizers during env cleanup - #30291

Merged
Jarred-Sumner merged 18 commits into
mainfrom
farm/61ff913a/fix-napi-create-error-during-finalizer-cleanup
Jul 8, 2026
Merged

Jarred-Sumner merged 18 commits into
mainfrom
farm/61ff913a/fix-napi-create-error-during-finalizer-cleanup

Conversation

@robobun

@robobun robobun commented May 5, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #30286.

Repro

gitnexus analyze on a synthetic repo with 15 trivial Java files reliably panicked at worker exit:

panic: NAPI FATAL ERROR: Error::New napi_create_error
  Bun::NapiFinalizer::call         napi_finalizer.cpp:13
  Zig::NapiRef::callFinalizer       napi.h:742
  NapiEnv::BoundFinalizer::call     napi.h:460
  NapiEnv::cleanup                  napi.h:211
  napi_internal_cleanup_env_cpp     napi.cpp:2993
  web_worker.exitAndDeinit ...

Cause

Two interacting bugs:

  1. NapiEnv::cleanup did not clear pending exceptions between finalizers. If finalizer N leaked a JSC VM exception (a common thing when a finalizer calls into JS via napi_call_function, or internally hits an allocation failure), finalizer N+1 inherited it. Node.js does the opposite — finalizers run with no JS frame above them to catch, so each one starts from a clean exception state.

  2. createErrorWithNapiValues refused to run with a pending VM exception. Node.js's napi_create_error is a pure value-producing function with no such check. Because Bun's napi_is_exception_pending deliberately skips the VM-scope check during cleanup, node-addon-api's Error::New(env) helper saw "no pending exception", took the "create a new error" branch, got back napi_pending_exception from createErrorWithNapiValues, and NAPI_FATAL_IF_FAILED fired napi_fatal_error.

Layer 1 can also surface as panic: A C++ exception occurred during process teardown: a finalizer's leaked exception unwinds out of NapiEnv::cleanup → napi_internal_cleanup_env_cpp → VirtualMachine.onExit / globalExit and hits std::set_terminate. That teardown signature shows up in several NAPI-module reports (@huggingface/transformers / onnxruntime-node, bun:sqlite); those are listed as "may fix" below rather than auto-closed, since none has been shown failing on main and passing on this branch (they were all reported on macOS).

Fix

  • NapiEnv::cleanup: clear VM and env pending exceptions at the start of cleanup, after the cleanup-hook drain, after each finalizer, and after the instance-data finalizer via a new clearExceptionsBetweenFinalizers helper. Uses DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException() so the EXCEPTION_SCOPE_VERIFICATION bookkeeping gets reset too — leaving it uncleared would trip debug asserts in the next finalizer's napi calls. The start/after-drain clears cover the case where the exception predates the finalizer phase (a cleanup hook or a terminated Worker's teardown promoted a scheduled exception onto the VM), which is the node-canvas shape reported in the issue thread (panic: NAPI FATAL ERROR: Error::Error napi_create_object with [email protected] + worker.terminate()).
  • createErrorWithNapiValues: drop the DECLARE_THROW_SCOPE + RETURN_IF_EXCEPTION at entry. The string inputs are already validated as strings before getString() is called, so nothing in the body can throw.

Verification

New test/napi/napi-app/test_finalizer_create_error.c addon, driven by test/napi/napi.test.ts:

  • Wraps two JS objects.
  • First-to-finalize finalizer calls a throwing JS function via napi_call_function, leaves the VM exception pending.
  • Second-to-finalize finalizer calls napi_create_error and prints the returned status.

Before the patch: status is 10 (napi_pending_exception) and debug ASAN aborts on the leaked exception first. After the patch: status is 0 (napi_ok) and the process exits 0.

A second test covers the pre-finalizer-phase leak: the addon registers a cleanup hook that schedules an exception via napi_throw_error and promotes it onto the VM through napi_call_function's prologue, then the wrap finalizer probes napi_create_error. Fails without the start/after-drain clears, passes with them.

Gate: bun bd test test/napi/napi.test.ts -t "30286" passes with the fix, fails (either an ASAN assertion abort on debug, or a non-zero status on release) without it.

Also verified the [email protected] + worker.terminate() repro from the issue thread: panics on the unfixed build, exits 0 with this PR.

Related

Only Closes #30286 auto-closes on merge — it's the one backed by the regression tests here. The issues below are may fix / related only; none was shown failing on main and passing on this branch (all were reported on macOS), so they are intentionally not Fixes lines.

@robobun

robobun commented May 5, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:45 AM PT - Jul 7th, 2026

❌ @Jarred-Sumner, your commit 03d7a37 has some failures in Build #69813 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 30291

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

bun-30291 --bun

@github-actions github-actions Bot added the claude label May 5, 2026
@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds VM-level exception clearing during N-API environment teardown and removes throw-scope reliance when creating errors. Introduces a clearExceptionsBetweenFinalizers helper, invokes it between finalizers in NapiEnv::cleanup, includes TopExceptionScope, and adds a regression N-API test and build target validating napi_create_error during cleanup.

Changes

NAPI Finalizer Exception Handling

Layer / File(s) Summary
VM Exception helper / include
src/jsc/bindings/napi.cpp
Adds #include <JavaScriptCore/TopExceptionScope.h> and defines void NapiEnv::clearExceptionsBetweenFinalizers() which calls DECLARE_TOP_EXCEPTION_SCOPE(m_vm).clearException() and clears m_pendingException.
Core behavior: error creation & pending check
src/jsc/bindings/napi.cpp
createErrorWithNapiValues no longer creates a throw scope; it obtains code/message strings via non-throwing getString and constructs the Error via createErrorWithCode without throw-scope bookkeeping. napi_is_exception_pending is updated to use DECLARE_TOP_EXCEPTION_SCOPE(vm) when inspecting VM exceptions during cleanup.
Cleanup wiring
src/jsc/bindings/napi.h
NapiEnv::cleanup() updated to call clearExceptionsBetweenFinalizers() after each finalizer invocation and after running/clearing instanceDataFinalizer() (helper declared in NapiEnv; defined out-of-line to confine TopExceptionScope usage).
Build target
test/napi/napi-app/binding.gyp
Adds new target test_finalizer_create_error to build the regression native addon with the same include/dependency settings and N-API flags used by other tests.
Regression addon & test
test/napi/napi-app/test_finalizer_create_error.c, test/napi/napi.test.ts
Adds native test module that installs two finalizers (one that leaves a pending VM exception via napi_call_function, then one that calls napi_create_error and prints create_error_status=<n>), and a JS test that runs the addon and asserts create_error_status=0, exit code 0, and empty stderr.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: clearing pending exceptions between finalizers during environment cleanup, which is the main technical fix addressing the crash.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from linked issues: clearing VM/env exceptions between finalizers (#30286), fixing crash during worker cleanup (#19917, #26081, #28557), and ensuring napi_create_error is safe during cleanup (supersedes #28248, #29785).
Out of Scope Changes check ✅ Passed All changes are in-scope: napi.cpp/napi.h modifications implement the core fix, test_finalizer_create_error.c/binding.gyp add regression test infrastructure, and napi.test.ts adds the verification test—all directly supporting the finalizer cleanup fix.
Description check ✅ Passed The description is detailed and covers the bug, fix, and verification, though it uses custom section headings instead of the template's exact titles.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/napi/napi.test.ts`:
- Around line 720-721: Remove the negative assertions that check stderr for
"panic" and "NAPI FATAL" and rely on the existing positive assertions (the
create_error_status=0 and exitCode=0 checks) to validate the test; specifically
delete or comment out the two lines using expect(stderr).not.toContain("panic")
and expect(stderr).not.toContain("NAPI FATAL") (or convert them to non-failing
diagnostic logs) so the test follows the guideline of asserting success rather
than absence of panic strings.
🪄 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: 689b52ca-94bc-4d1e-940d-78eb94b9b3cc

📥 Commits

Reviewing files that changed from the base of the PR and between b009453 and 6de6df8.

📒 Files selected for processing (5)
  • src/jsc/bindings/napi.cpp
  • src/jsc/bindings/napi.h
  • test/napi/napi-app/binding.gyp
  • test/napi/napi-app/test_finalizer_create_error.c
  • test/napi/napi.test.ts

Comment thread test/napi/napi.test.ts Resolved
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Found 5 issues this PR may fix:

  1. panic: Error::New napi_get_last_error_info #20663 - Panic in Error::New via napi_get_last_error_info — exact same createErrorWithNapiValues crash path this PR fixes
  2. bun test panics after tests pass when using mock.module() with bun:sqlite and repeated SQLite create/close cycles on macOS #28557 - "A C++ exception occurred" panic during process teardown after tests pass with NAPI modules (bun:sqlite + mock.module)
  3. Bun test crashes when using @huggingface/transformers #26081 - "A C++ exception occurred" during exit after tests pass with @huggingface/transformers ONNX NAPI module
  4. Bun v1.2.14 (6a363a38) on macOS Silicon crashes with the message "A C++ exception occurred" when attempting to use the huggingface/transformers pipeline for "feature-extraction". #19917 - "A C++ exception occurred" during process exit with @huggingface/transformers NAPI module loaded
  5. npx bun test crash #21068 - "A C++ exception occurred" panic during bun test with NAPI module loaded via process_dlopen

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #20663
Fixes #28557
Fixes #26081
Fixes #19917
Fixes #21068

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(napi): make napi_create_error succeed when pending exception exists #29785 - Also fixes createErrorWithNapiValues rejecting calls when a pending exception exists in napi.cpp
  2. fix(napi): don't fail napi_create_error when a pending exception exists #28248 - Earlier attempt at the same napi_create_error pending-exception fix in napi.cpp

🤖 Generated with Claude Code

Comment thread test/napi/napi.test.ts Resolved
robobun added a commit that referenced this pull request May 5, 2026
Per CLAUDE.md: 'Never write tests that check for no panic or uncaught
exception in the test output. These tests will never fail in CI.'

The positive assertions (stdout == 'create_error_status=0' and
exitCode == 0) already fail decisively if the finalizer panics --
stdout is empty on a crash. Also added stderr == '' as a positive
absence-of-diagnostic check.

Addresses CodeRabbit review on #30291.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing the test-assertion nit. No bugs found, but this changes JSC exception-scope handling in the NAPI cleanup path (dropping the throw scope in createErrorWithNapiValues and clearing VM exceptions between finalizers), so it'd be good to have someone with JSC internals context confirm the scope semantics before merge.

Extended reasoning...

Overview

This PR touches two C++ files in the NAPI bindings (src/jsc/bindings/napi.cpp, src/jsc/bindings/napi.h) plus a new C test addon, a binding.gyp target, and a regression test in napi.test.ts. The functional changes are: (1) createErrorWithNapiValues drops its DECLARE_THROW_SCOPE and three RETURN_IF_EXCEPTION guards so napi_create_error succeeds even with a pending VM exception, matching Node.js; and (2) NapiEnv::cleanup() now calls a new clearExceptionsBetweenFinalizers() helper (which uses DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException() and clears m_pendingException) after each finalizer and after the instance-data finalizer.

Security risks

None apparent — this is internal exception-state hygiene during env teardown, not an auth/input-validation surface.

Level of scrutiny

High. This is production-critical native-bindings code on the worker/process teardown path, and the change alters JSC exception-scope semantics. Two specific points deserve a JSC-aware reviewer's eye: (a) the claim that JSValue::getString(globalObject) on a value already verified isString() can never throw (rope resolution / OOM edge cases) and that createErrorWithCode is safe without a surrounding throw scope under EXCEPTION_SCOPE_VERIFICATION; and (b) that DECLARE_TOP_EXCEPTION_SCOPE(m_vm).clearException() on a temporary is the right idiom here vs. a longer-lived scope or a catch scope. The reasoning in the PR description is thorough and plausible, but these are exactly the kind of invariants that are easy to get subtly wrong.

Other factors

The PR includes a targeted regression test that exercises both layers of the fix, and the author already addressed my earlier style nit (commit eddff53 swapped the negative panic-string assertions for expect(stderr).toBe("")). The PR also claims to close several long-standing crash reports (#19917, #26081, #28557), which raises the impact of getting the fix right. No CODEOWNERS cover these files. Given the subtlety of JSC exception-scope bookkeeping and the breadth of the claimed fixes, I'm deferring rather than approving.

Comment thread src/jsc/bindings/napi.h Resolved
Comment thread src/jsc/bindings/napi.cpp Resolved
@robobun

robobun commented May 9, 2026

Copy link
Copy Markdown
Collaborator Author

Also fixes #30431 — the macOS shutdown crash there (panic(main thread): A C++ exception occurred after all tests pass on onnxruntime-node) is the same layer-1 exception-leak path this PR fixes. The Linux Segmentation fault at address 0x1A half of that issue was a separate, already-merged fix (#29981 / 65a0b2f7f6, re-entrant napi_module_register iterator invalidation).

@HaleTom

HaleTom commented May 9, 2026 •

Copy link
Copy Markdown

@dylan-conway @Jarred-Sumner this PR needs a maintainer to unblock CI and merge.

The only failing check is "Lint JavaScript" — 3 oxlint Unsafe declaration merging between classes and interfaces errors in src/js/. The PR only touches C++ (napi.cpp, napi.h, napi_finalizer.cpp) and test files — zero changes to src/js/. I verified locally: the PR's merge ref passes bun lint with 0 warnings, 0 errors. This is a stale CI run against an old merge base (the base has moved forward since the run was triggered).

Could someone please re-run ?

robobun and others added 9 commits June 2, 2026 05:37
Tree-sitter inside node:worker_threads deterministically panicked at
worker exit once ~15 finalizers had run:

  panic: NAPI FATAL ERROR: Error::New napi_create_error
  at NapiEnv::cleanup -> NapiRef::callFinalizer -> NapiFinalizer::call

Two things were broken. First, NapiEnv::cleanup ran each finalizer
without clearing any pending exception after it returned, so a throw
from one finalizer leaked into the next. Node.js doesn't do that --
finalizers run with no JS frame above them to catch, so propagating
the exception is pointless and only confuses later finalizers.

Second, createErrorWithNapiValues refused to produce an Error value
while a VM-level exception was pending (DECLARE_THROW_SCOPE +
RETURN_IF_EXCEPTION at the top). Node.js's napi_create_error has no
such check -- it is a pure value-producing function. Because
napi_is_exception_pending deliberately skips the VM-scope check
during cleanup for safety, node-addon-api's Error::New(env) saw 'no
pending exception', fell through to napi_create_error, got back
napi_pending_exception, and tripped NAPI_FATAL_IF_FAILED ->
napi_fatal_error.

Fix both layers: clear env/VM exceptions after each finalizer in
NapiEnv::cleanup, and drop the pending-exception check from
createErrorWithNapiValues. Matches Node.js on both counts.

Regression test: a standalone addon wraps two JS objects; the first
finalizer calls a throwing JS function via napi_call_function
(leaving the JSC VM exception pending without clearing it), the
second calls napi_create_error and prints the returned status.
Before the fix the status is 10 (napi_pending_exception) and the
debug ASAN build aborts on the leaked exception first; after the
fix the status is 0 (napi_ok) and the process exits cleanly.

Closes #30286.
Related: #22259, #28248, #29785.
Per CLAUDE.md: 'Never write tests that check for no panic or uncaught
exception in the test output. These tests will never fail in CI.'

The positive assertions (stdout == 'create_error_status=0' and
exitCode == 0) already fail decisively if the finalizer panics --
stdout is empty on a crash. Also added stderr == '' as a positive
absence-of-diagnostic check.

Addresses CodeRabbit review on #30291.
Declared inline in napi.h it was getting instantiated in every TU that
pulled in napi_external.h. On Windows aarch64 (build 51887's only
real failure), the TopExceptionScope ctor/dtor are JS_EXPORT_PRIVATE
under ENABLE_EXCEPTION_SCOPE_VERIFICATION; that broke build-cpp.
Confining the body to napi.cpp means TopExceptionScope is touched
from exactly one TU, matching how every other caller
(ErrorStackTrace.cpp, NodeVMModule.cpp, MessagePort.cpp, ...) uses
it today.
Comment thread src/jsc/bindings/napi.h Resolved
robobun and others added 2 commits June 8, 2026 00:27
drain() ran hooks back-to-back, so a hook that leaked a VM exception
poisoned the next hook's napi calls. Apply the same per-callback clear
the finalizer loop uses, making cleanup()'s documented invariant
(every native teardown callback starts from a clean exception state)
hold for hooks as well.
@HaleTom

HaleTom commented Jun 10, 2026

Copy link
Copy Markdown

Thanks @basedcorp99 for drawing my attention back to this PR.

@dylan-conway @Jarred-Sumner robobun says the PR itself is green -- would you please take a look?

Cheers!

@robobun

robobun commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

Independent confirmation of this PR's analysis, from triaging the same Sentry crash (BUN-2ZQ9 / BUN-38NS, NAPI FATAL ERROR: Error::New napi_create_error, ~23/day on 1.3.14). I reproduced it on 1.4.0 with a raw-C addon that replays node-addon-api's Error::New(env) call sequence from a napi_wrap finalizer during env cleanup, in two states: after worker.terminate() landed mid-execution (thrown TerminationException left pending), and with an exception leaked by an env cleanup hook. Node 24.3.0 returns napi_ok for every call in both states; bun fails napi_create_error with napi_pending_exception while napi_is_exception_pending reports false, which is exactly the mismatch NAPI_FATAL_IF_FAILED turns into the abort.

While verifying the fix against Node I found the same class extends past createErrorWithNapiValues, so the abort comes back one call later once that function is fixed:

  • napi_create_reference, napi_get_reference_value, napi_reference_ref, napi_reference_unref still use NAPI_PREAMBLE. Node omits it for the whole reference family ("V8 calls here cannot throw JS exceptions"). node-addon-api's Error::Error stores the new error in a reference and ThrowAsJavaScriptException reads it back, each with its own NAPI_FATAL_IF_FAILED, so with only the create-error fix the crash moves to Error::Error napi_create_reference.
  • napi_create_string_latin1 fails with a pending exception even in release builds (BunString__toJS has RETURN_IF_EXCEPTION), and napi_create_string_utf8 with a non-ASCII message trips EXCEPTION_ASSERT/assertNoException in BunString__createUTF8ForJS on debug builds. The between-finalizer clearing in this PR masks both inside NapiEnv::cleanup, but they stay broken in other pending-exception contexts (Node allows these calls anywhere).
  • For the worker case, clearing at cleanup entry happens after Process__dispatchOnExit, so the pending TerminationException still skips the worker's process.on('exit') handlers (dispatchExitInternal bails on hasExceptionsAfterHandlingTraps), and any DeferTermination scope entered in that window trips ASSERT(vm.hasTerminationRequest()) in VMTraps::deferTerminationSlow on debug builds, because worker shutdown cleared the request bit but not the thrown exception. Switching web_worker.rs shutdown from clear_has_termination_request() to the existing JSGlobalObject__clearTerminationException helper (already used by the REPL and test runner for exactly this) fixes the state at the source.

Branch with those fixes plus tests comparing against Node for all of the above: https://github.com/oven-sh/bun/branches/all?query=farm/0a365ff8/napi-error-new-finalizers (farm/0a365ff8/napi-error-new-finalizers, f0409d5). The fail-before proof: on an unfixed debug build the new test aborts on the asserts above; on an unfixed release build it gets create_error=10 create_string_latin1=9. Happy for any of it to be folded in here, or rebased as a follow-up once this lands; not opening a separate PR since this one already covers the headline crash.

@HaleTom

HaleTom commented Jun 11, 2026

Copy link
Copy Markdown

@robobun I'm a bit maxxed out ATM and my agent isn't as good as you are.

I'll take the W on the headline fix and give you the pleasure of committing your own work :)

@HaleTom

HaleTom commented Jun 14, 2026

Copy link
Copy Markdown

@dylan-conway @Jarred-Sumner could you please unleash robobun to merge this?

@robobun said:

The PR diff itself is green. This needs a maintainer to merge (the unrelated pre-existing failure will clear on its own or needs a separate fix)

@robobun

robobun commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator Author

Appreciate the vote of confidence, @HaleTom! I don't have merge rights on this repo (no maintainer/admin bit), and the PR currently shows BLOCKED under branch protection, so the merge button genuinely needs a maintainer.

Everything on my side is ready: diff is green, main is merged in, all review threads resolved, regression tests + the [email protected] worker-terminate repro verified. Over to @dylan-conway / @Jarred-Sumner whenever you have a moment.

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can't simply ignore it - this will crash at runtime. We could use a catch scope if needed, but we can't ignore.

createErrorWithNapiValues must not fail on a pre-existing VM exception
(the #30286 fix), but getString() resolves rope strings and can throw
OutOfMemoryError -- returning napi_ok with that left pending on the VM
crashes at the next exception-scope check. Use a TopExceptionScope and
clear only an exception our own string resolution / error construction
raised, leaving any pre-existing exception (and termination exceptions)
untouched to match Node.js. Addresses review feedback.
@robobun

robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch — you're right, ignoring it is wrong. Fixed in e8a9257.

createErrorWithNapiValues now uses a TopExceptionScope (catch scope) instead of a throw scope. That keeps the #30286 fix (a pre-existing VM exception no longer forces an early napi_pending_exception return), but it no longer leaves a new exception pending: getString() resolves rope strings and can throw OutOfMemoryError, so after building the error I clear an exception only if one appeared that wasn't already pending on entry:

auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
// ...validate args...
JSC::Exception* preExisting = scope.exception();
auto wtf_code = js_code.isEmpty() ? WTF::String() : js_code.getString(globalObject);
auto wtf_message = js_message.getString(globalObject);
*result = toNapi(createErrorWithCode(vm, globalObject, wtf_code, wtf_message, type), globalObject);
if (scope.exception() && scope.exception() != preExisting)
    scope.clearExceptionExceptTermination();

So: a pre-existing exception is left untouched (matches Node, which never disturbs the caller's pending exception), a termination exception is never cleared (it must keep unwinding), and a new OOM from string resolution is cleared rather than leaked. Verified under bun bd (debug + ASAN, which aborts on an unchecked pending exception): the #30286 regression tests, the full napi suite, and the [email protected] + worker.terminate() repro from this thread all pass / exit 0.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

I went through the "Found 5 issues this PR may fix" list and tried to verify each one empirically. Test box is Linux x64; all five issues were reported on macOS, which limits what can be proven from here — details per issue below.

Verdicts

  1. panic: Error::New napi_get_last_error_info #20663 (panic: Error::New napi_get_last_error_info) — cannot verify: no reproduction. The issue contains only a bun.report trace, no repro steps. The failing call there is napi_get_last_error_info, not the napi_create_error / env-cleanup path this PR changes. Do not auto-close.
  2. bun test panics after tests pass when using mock.module() with bun:sqlite and repeated SQLite create/close cycles on macOS #28557 (bun:sqlite + mock.module teardown panic, macOS) — cannot verify from this box (macOS-only). I ran the issue's exact repro (ualtinok/bun-sqlite-mock-module-crash-repro, bun test src/search/tools.test.ts) on Linux x64 with an unmodified Bun 1.4.0-canary (c76d6b2, does not contain this PR): 23 pass, 0 fail, exit 0, no panic. The onnxruntime NAPI addon does load on this platform (verified napi-v3/linux/x64/onnxruntime_binding.node via dlopen), so the repro is exercised — the crash just never manifests on Linux. Without a failing baseline I can't demonstrate that this PR is what fixes it.
  3. Bun test crashes when using @huggingface/transformers #26081 (@huggingface/transformers, exit crash) — already closed (2026-05-13, "works on 1.3.13" per the reporter) — i.e. it stopped reproducing on a release that does not contain this PR. It should not carry a Fixes line here.
  4. Bun v1.2.14 (6a363a38) on macOS Silicon crashes with the message "A C++ exception occurred" when attempting to use the huggingface/transformers pipeline for "feature-extraction". #19917 (@huggingface/transformers pipeline in bun test, macOS) — cannot verify from this box (macOS-only). I built the issue's snippet (pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2') awaited inside a bun test test) and ran it on Linux x64 with the same unmodified Bun: 1 pass, exit 0, no panic. Same situation as bun test panics after tests pass when using mock.module() with bun:sqlite and repeated SQLite create/close cycles on macOS #28557.
  5. npx bun test crash #21068 (npx bun test with a Python-embedding native module) — cannot verify. The repro needs macOS plus a full rbt dev run environment; the panic there happens during module load (CPython process_dlopen), not during env-cleanup/teardown, so it's a different code path from this PR (the PR description already says the same).

Safe auto-close block: empty. None of the five reached a proven "fails on main, passes on this branch" from this box, so I can't justify adding any of the bot-suggested Fixes #N lines:

(none of #20663 #28557 #26081 #19917 #21068)

Heads-up on the current PR description: it already carries Fixes #19917, Fixes #26081, Fixes #28557.

  • Fixes #26081 should be dropped regardless — that issue is already closed as resolved on 1.3.13 without this PR, and a merge-time auto-close reference to it is misleading.
  • Fixes #19917 / Fixes #28557 are plausible (same A C++ exception occurred teardown signature, and the mechanism this PR fixes is real — the PR's own NAPI FATAL Error::New napi_create_error during Worker exitAndDeinit (NapiFinalizer cleanup) — deterministic at 15 jobs via gitnexus + tree-sitter #30286 regression test covers it), but nobody has yet shown either issue's repro failing on main and passing on this branch on macOS. If a maintainer can't do that macOS A/B, I'd downgrade those to "may fix / related" so an unfixed issue doesn't get auto-closed on merge. Closes #30286 is the one that's actually backed by a regression test in this PR.

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Agreed on all five — thanks for doing the empirical A/B. I've updated the PR description:

So on merge only #30286 closes. If someone later does the macOS A/B for #19917 or #28557 and confirms, those can be closed manually with the evidence.

KorenKrita added a commit to KorenKrita/magic-context that referenced this pull request Jul 7, 2026
Wire the OpenCode 'server.instance.disposed' SDK event to stop this instance's
process-resident resources (RPC server, dream-schedule timer registration,
auto-update abort controller). Desktop runs multiple plugin instances in one
process, each disposed independently, so cleanup is gated on the disposed
directory resolving to our own project identity — never tears down a sibling
instance's still-live resources. Also fixes a pre-existing leak: the dream-timer
registration cleanup was never captured.

Deliberately does NOT dispose the native ONNX embedding session here — forcing
onnxruntime-node's destructor on teardown makes the Bun N-API exit crash (cortexkit#95)
worse, not better (tracked upstream at oven-sh/bun#30291); the OS reclaims that
memory on exit. Pi keeps its own session_shutdown cleanup (no server.instance.
disposed event in Pi) — documented divergence.

Co-authored-by: Alfonso [Magic Context] <[email protected]>
@Jarred-Sumner
Jarred-Sumner merged commit cf5bf1b into main Jul 8, 2026
76 of 77 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/61ff913a/fix-napi-create-error-during-finalizer-cleanup branch July 8, 2026 08:22
kucukkanat added a commit to kucukkanat/edgewise that referenced this pull request Oct 7, 2026
Terminating a Bun worker that loaded onnxruntime-node crashed the whole
process with "panic: NAPI FATAL ERROR: Error::New napi_create_error"
(exit 133). The cause is in Bun, not the models: Worker.terminate()
leaves an exception pending on the VM, and the addon's
InferenceSessionWrap finalizer then calls Napi::Error::New, which Bun
refused to create and node-addon-api treats as fatal
(oven-sh/bun#30286, fixed by oven-sh/bun#30291 in Bun 1.4.0).

A worker that calls process.exit() on its own is torn down safely, so on
Bun < 1.4 terminate() now posts a close message and resolves on the
worker's close event. Everywhere else it terminates directly.
terminate() now returns a Promise.

Also reject calls that were still in toWire() when terminate() ran;
before, they registered their handler afterwards and never settled.
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.

NAPI FATAL Error::New napi_create_error during Worker exitAndDeinit (NapiFinalizer cleanup) — deterministic at 15 jobs via gitnexus + tree-sitter

4 participants