Skip to content

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

Description

@kraenhansen

Follow-up to #398, tracking the TODO in packages/host/cpp/HermesNapiHost.cpp (HostContext::fatalException).

Current behavior

The hermes_napi_host::fatal_exception hook stringifies the error (stack, falling back to coercion), logs it through the host logger, and abort()s — the same observable outcome as Hermes' null-host default, just with better diagnostics. In Node, napi_fatal_exception instead triggers process.emit('uncaughtException'), which is observable and handleable. node-addon-api calls napi_fatal_exception whenever an exception escapes a thread-safe-function callback, so today a single throwing tsfn callback hard-kills a React Native app with no LogBox and no JS-side handler getting a say.

Returning is legal — only napi_fatal_error is noreturn

Worth stating explicitly because the two fatal APIs are easy to conflate:

  • napi_fatal_error is NAPI_NO_RETURN void (node_api.h) — it has no env and no error value, and must not return. Our override in RuntimeNodeApi.cpp logs and aborts; that stays as-is.

  • napi_fatal_exception is a plain napi_status-returning function, and Hermes' implementation at the pinned commit explicitly supports the host hook returning normally (API/napi/hermes_napi_error.cpp):

    if (env->host_ && env->host_->fatal_exception) {
      env->host_->fatal_exception(env->host_->data, env, err);
      return napi_clear_last_error(env);
    }

    The hook is void (*)(void *data, napi_env env, napi_value err) with no noreturn contract; after it returns, the addon's napi_fatal_exception call returns napi_ok — matching Node, where emitting 'uncaughtException' returns to the caller (the process only dies if no handler is installed).

Proposed approach (open to alternatives)

fatal_exception runs on the JS thread with a live env and err valid for the duration of the call, so the routing can be done synchronously with pure Node-API against the passed env — keeping HermesNapiHost.cpp free of React Native/JSI includes:

  1. napi_get_globalnapi_get_named_property(global, "ErrorUtils")napi_get_named_property(errorUtils, "reportFatalError"), type-checking each step.
  2. napi_call_function(env, errorUtils, reportFatalError, 1, &err, nullptr) and return normally on success.
  3. On any failure — ErrorUtils absent (non-RN embedder, early startup), the handler itself throwing (napi_pending_exception) — fall back to today's stringify + log_error + abort(), and guard reentrancy (a handler that itself triggers napi_fatal_exception) with a flag that short-circuits straight to the fallback.

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

This also becomes testable on-device: a test addon calls napi_fatal_exception while the driver has a temporary ErrorUtils.setGlobalHandler installed, asserting the handler receives the error, the call returns napi_ok, and the app survives.

Metadata

Metadata

Assignees

Labels

AutomatableAn issue we expect to be fixed using automation.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions