Skip to content

Route napi_fatal_exception through ErrorUtils.reportFatalError instead of aborting - #432

Merged
kraenhansen merged 1 commit into
nextfrom
claude/issue-402-fatal-exception-routing
Aug 13, 2026
Merged

Route napi_fatal_exception through ErrorUtils.reportFatalError instead of aborting#432
kraenhansen merged 1 commit into
nextfrom
claude/issue-402-fatal-exception-routing

Conversation

@kraenhansen

Copy link
Copy Markdown
Collaborator

Closes #402.

⚠️ Untested — not compiled, not run on-device

This is genuinely correctness-sensitive C++ (fatal-error handling on the JS
thread) and I could not build or exercise it in this environment: this
worker has no Android NDK / Apple toolchain, so HermesNapiHost.cpp was
never compiled, let alone linked against a real hermes_napi_host and run
through the described on-device scenario (a test addon calling
napi_fatal_exception under a temporary ErrorUtils.setGlobalHandler).
Opening as a draft on purpose — please review the Node-API call sequence
and control flow carefully, and verify on-device, before merge.

What changed

HostContext::fatalException (packages/host/cpp/HermesNapiHost.cpp)
previously stringified the error and called abort() unconditionally. Per
the issue, napi_fatal_exception (unlike the noreturn napi_fatal_error,
which is untouched — that's #428's file/territory, not modified here) is a
plain napi_status-returning function, and the pinned Hermes commit's
hermes_napi_error.cpp explicitly supports the host hook returning
normally. So the hook now:

  1. Attempts global.ErrorUtils.reportFatalError(err) via
    napi_get_globalnapi_get_named_property(global, "ErrorUtils")
    napi_get_named_property(errorUtils, "reportFatalError") (type-checked
    at each step: must resolve to an object, then a function) →
    napi_call_function.
  2. On success, returns normally — Hermes then returns napi_ok to the
    addon, matching Node's process.emit('uncaughtException') returning to
    the caller when a handler is installed.
  3. On any failure — ErrorUtils/reportFatalError absent or the wrong
    type, or the call itself throwing — clears any pending exception
    (napi_is_exception_pending / napi_get_and_clear_last_exception) and
    falls back to the previous stringify + log_error + abort() path.
  4. Guards reentrancy with a bool member on HostContext
    (inFatalException_): if the ErrorUtils handler itself triggers
    another napi_fatal_exception, the nested call skips straight to the
    fallback instead of recursing. fatalException always runs
    synchronously on the JS thread, so a plain member (no atomics/
    thread_local) is enough.

HermesNapiHost.cpp/.hpp remain free of React Native/JSI includes — all
routing goes through the passed napi_env using plain Node-API calls, all
of which (napi_get_global, napi_get_named_property, napi_typeof,
napi_call_function, napi_is_exception_pending,
napi_get_and_clear_last_exception) are already generated into
weak-node-api from the full node-api-headers symbol set (napi_get_global
itself was already used elsewhere in CxxNodeApiHostModule.cpp), so no
weak-node-api changes were needed.

Resulting semantics: in dev, RN's default handler shows LogBox with the
real error and stack; in release, it rethrows into the native crash path;
apps can install ErrorUtils.setGlobalHandler to observe/handle, the moral
equivalent of listening for 'uncaughtException'.

Changeset

Added .changeset/route-fatal-exception-through-errorutils.md as minor
for react-native-node-api — this is an observable behavior change for any
addon/app that relied on the previous immediate abort() (per repo
convention, this is a pre-release/next-targeted changeset per
.changeset/pre.json).

What I did / did not verify

  • pnpm install && pnpm run build (TypeScript project build) — clean.
  • pnpm exec prettier --check on the new changeset file — clean.
  • pnpm run lint — the only failures are 4 pre-existing
    @typescript-eslint/no-unsafe-* errors in apps/test-app/App.tsx,
    unrelated to this change (confirmed via git diff origin/next touching
    nothing in that file).
  • No C++ compilation of HermesNapiHost.cpp/.hpp — no NDK/Apple
    toolchain available here.
  • No on-device test of the actual routing behavior (LogBox
    appearing, ErrorUtils.setGlobalHandler receiving the error, the call
    returning napi_ok, the app surviving) — the issue's own suggested test.

Recommended follow-up

The issue suggests a test addon that calls napi_fatal_exception under a
temporary ErrorUtils.setGlobalHandler. packages/node-addon-examples
already has a tests/threadsafe-function addon that could be a template,
but I deliberately did not add a new native test addon myself: writing
untested native C++ in an environment where I can't compile or run it adds
more unverified surface area rather than de-risking this change. I'd
recommend adding that on-device test as a fast-follow once this PR's core
logic has been reviewed/verified.

Files touched

  • packages/host/cpp/HermesNapiHost.cpp
  • packages/host/cpp/HermesNapiHost.hpp
  • .changeset/route-fatal-exception-through-errorutils.md

(packages/host/cpp/RuntimeNodeApi.cpp was intentionally left untouched —
that's issue #428's territory.)


🤖 Generated with Claude Code

https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm


Generated by Claude Code

@kraenhansen kraenhansen added Apple 🍎 Anything related to the Apple platform (iOS, macOS, Cocoapods, Xcode, XCFrameworks, etc.) Android 🤖 Anything related to the Android platform (Gradle, NDK, Android SDK) Host 🏡 Our `react-native-node-api-modules` package labels Aug 13, 2026 — with Claude
HostContext::fatalException previously stringified the error and called
abort() unconditionally, matching Hermes' null-host default but giving
node-addon-api's tsfn error path (which calls napi_fatal_exception whenever
an exception escapes a thread-safe-function callback) no chance of being
observed or handled — a single throwing tsfn callback hard-killed the app
with no LogBox and no JS-side handler getting a say.

napi_fatal_exception (unlike the noreturn napi_fatal_error) is a plain
napi_status-returning function, and the pinned Hermes commit's
hermes_napi_error.cpp explicitly supports the host hook returning normally,
so routing can be done synchronously against the passed env with plain
Node-API calls, keeping HermesNapiHost.cpp free of React Native/JSI
includes:

- Attempt global.ErrorUtils.reportFatalError(err) via napi_get_global +
  napi_get_named_property (x2, type-checked at each step) +
  napi_call_function.
- On success, return normally (napi_ok reaches the addon), matching Node's
  process.emit('uncaughtException') returning to the caller when a handler
  is installed.
- On any failure (ErrorUtils/reportFatalError absent or not the right
  type, or the call itself throwing) fall back to the previous stringify +
  log_error + abort() path, clearing any pending exception first so the
  fallback's own Node-API calls aren't defeated by a stale exception.
- Guard reentrancy with a HostContext member flag: if the ErrorUtils
  handler itself triggers another napi_fatal_exception, the nested call
  skips straight to the fallback instead of recursing.

Adds a "minor" changeset for react-native-node-api: this is an observable
behavior change for addons/apps that relied on the previous immediate
abort.

Not compiled or exercised on-device in this environment (no Android/iOS
toolchain here) — see the PR description for what remains to be verified.

Closes #402

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm
@kraenhansen
kraenhansen force-pushed the claude/issue-402-fatal-exception-routing branch from d7d58c3 to 1bf5dfe Compare August 13, 2026 10:33
@kraenhansen kraenhansen self-assigned this Aug 13, 2026
@kraenhansen
kraenhansen marked this pull request as ready for review August 13, 2026 11:07
@kraenhansen
kraenhansen merged commit 715a24e into next Aug 13, 2026
16 checks passed
@kraenhansen
kraenhansen deleted the claude/issue-402-fatal-exception-routing branch August 13, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Android 🤖 Anything related to the Android platform (Gradle, NDK, Android SDK) Apple 🍎 Anything related to the Apple platform (iOS, macOS, Cocoapods, Xcode, XCFrameworks, etc.) Host 🏡 Our `react-native-node-api-modules` package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Route napi_fatal_exception through React Native's error handling instead of aborting

2 participants