Route napi_fatal_exception through ErrorUtils.reportFatalError instead of aborting - #432
Merged
Merged
Conversation
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
force-pushed
the
claude/issue-402-fatal-exception-routing
branch
from
August 13, 2026 10:33
d7d58c3 to
1bf5dfe
Compare
kraenhansen
marked this pull request as ready for review
August 13, 2026 11:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #402.
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.cppwasnever compiled, let alone linked against a real
hermes_napi_hostand runthrough the described on-device scenario (a test addon calling
napi_fatal_exceptionunder a temporaryErrorUtils.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. Perthe issue,
napi_fatal_exception(unlike thenoreturnnapi_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'shermes_napi_error.cppexplicitly supports the host hook returningnormally. So the hook now:
global.ErrorUtils.reportFatalError(err)vianapi_get_global→napi_get_named_property(global, "ErrorUtils")→napi_get_named_property(errorUtils, "reportFatalError")(type-checkedat each step: must resolve to an object, then a function) →
napi_call_function.napi_okto theaddon, matching Node's
process.emit('uncaughtException')returning tothe caller when a handler is installed.
ErrorUtils/reportFatalErrorabsent or the wrongtype, or the call itself throwing — clears any pending exception
(
napi_is_exception_pending/napi_get_and_clear_last_exception) andfalls back to the previous stringify +
log_error+abort()path.boolmember onHostContext(
inFatalException_): if theErrorUtilshandler itself triggersanother
napi_fatal_exception, the nested call skips straight to thefallback instead of recursing.
fatalExceptionalways runssynchronously on the JS thread, so a plain member (no atomics/
thread_local) is enough.HermesNapiHost.cpp/.hppremain free of React Native/JSI includes — allrouting goes through the passed
napi_envusing plain Node-API calls, allof 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 intoweak-node-apifrom the fullnode-api-headerssymbol set (napi_get_globalitself was already used elsewhere in
CxxNodeApiHostModule.cpp), so noweak-node-apichanges 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.setGlobalHandlerto observe/handle, the moralequivalent of listening for
'uncaughtException'.Changeset
Added
.changeset/route-fatal-exception-through-errorutils.mdas minorfor
react-native-node-api— this is an observable behavior change for anyaddon/app that relied on the previous immediate
abort()(per repoconvention, 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 --checkon the new changeset file — clean.pnpm run lint— the only failures are 4 pre-existing@typescript-eslint/no-unsafe-*errors inapps/test-app/App.tsx,unrelated to this change (confirmed via
git diff origin/nexttouchingnothing in that file).
HermesNapiHost.cpp/.hpp— no NDK/Appletoolchain available here.
appearing,
ErrorUtils.setGlobalHandlerreceiving the error, the callreturning
napi_ok, the app surviving) — the issue's own suggested test.Recommended follow-up
The issue suggests a test addon that calls
napi_fatal_exceptionunder atemporary
ErrorUtils.setGlobalHandler.packages/node-addon-examplesalready has a
tests/threadsafe-functionaddon 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.cpppackages/host/cpp/HermesNapiHost.hpp.changeset/route-fatal-exception-through-errorutils.md(
packages/host/cpp/RuntimeNodeApi.cppwas intentionally left untouched —that's issue #428's territory.)
🤖 Generated with Claude Code
https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm
Generated by Claude Code