Skip to content

Node-API (JSI): surface script exceptions from Napi::Eval as Napi::Error instead of aborting - #246

Open
matthargett wants to merge 1 commit into
BabylonJS:mainfrom
rebeckerspecialties:jsi-eval-napi-error
Open

matthargett wants to merge 1 commit into
BabylonJS:mainfrom
rebeckerspecialties:jsi-eval-napi-error

Conversation

@matthargett

@matthargett matthargett commented Sep 16, 2026

Copy link
Copy Markdown

Problem

On the V8JSI backend Napi::Eval calls Runtime::evaluateJavaScript directly, and a script exception surfaces as facebook::jsi::JSError. Every other engine throws Napi::Error, and AppRuntime::Dispatch only catches that (catch (...)std::abort), so a throw reaching a dispatched Eval on JSI aborted the whole process with exit 3 — seen on Win32_x64_JSI while adding a test in #239 (which had to gate that test off on JSI).

Changes

  • Napi::Eval converts jsi::JSError into Napi::Error carrying the thrown value (the JSI Napi::Error(napi_env, jsi::Value) constructor), and any other jsi::JSIException into a Napi::Error with its message.
  • NodeApi.EvalThrowIsCatchable (all engines): throw new Error('boom') from Napi::Eval is caught as Napi::Error, Message() reads boom, and the runtime keeps evaluating. The primitive-throw variant is covered by JSC Node-API: reference primitives and coerce property receivers (fixes a RELEASE_ASSERT on non-object exceptions) #239's NodeApi.PrimitiveExceptionSurvivesNativeCatch; once both land its JSI gate can go.

Verified locally on macOS (JavaScriptCore, where the test already passed); the JSI path is exercised by the fork twin's Win32/UWP JSI jobs: rebeckerspecialties#30.

The V8JSI shim let facebook::jsi::JSError escape from evaluateJavaScript
unconverted. Every other engine throws Napi::Error for a script
exception, and AppRuntime's dispatch treats anything else as fatal, so a
`throw` reaching a dispatched Eval on JSI aborted the process (exit 3).
Convert JSError to Napi::Error carrying the thrown value, and other JSI
exceptions to a Napi::Error with their message.

Copilot AI 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.

🟡 Changes recommended

The current JSI exception conversion can still fail for primitive throws (due to Napi::Error(napi_env, jsi::Value) requiring an object), and the new unit test can deadlock if the dispatched lambda exits via an unexpected exception path.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR fixes a backend inconsistency in the Node-API JSI shim by translating script-thrown exceptions from facebook::jsi into Napi::Error, preventing AppRuntime::Dispatch from treating them as fatal and aborting the process.

Changes:

  • Update Napi::Eval (JSI backend) to catch facebook::jsi::JSError/JSIException and rethrow as Napi::Error.
  • Add a unit test asserting that throw new Error('boom') from Napi::Eval is catchable as Napi::Error and that the runtime can continue evaluating afterward.
File summaries
File Description
Tests/UnitTests/Shared/Shared.cpp Adds regression test ensuring exceptions from Napi::Eval are catchable and do not halt subsequent evaluation.
Core/Node-API-JSI/Source/env.cc Wraps JSI exceptions thrown by evaluateJavaScript into Napi::Error to align behavior with other engines and avoid aborts in dispatch.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +24 to +29
catch (const facebook::jsi::JSError& error)
{
// A script exception (any thrown value, primitives included) has to reach callers as the
// Napi::Error the other engines throw; AppRuntime's dispatch treats anything else as fatal.
throw Napi::Error{env_ptr, facebook::jsi::Value{env_ptr->rt, error.value()}};
}
Comment on lines +841 to +859
std::promise<bool> outcome;
runtime.Dispatch([&outcome](Napi::Env env) {
bool caught{false};
std::string message;
try
{
Napi::Eval(env, "throw new Error('boom');", "eval-throw.js");
}
catch (const Napi::Error& error)
{
caught = true;
message = error.Message();
}
const auto sum = Napi::Eval(env, "1 + 1", "eval-throw.js");
outcome.set_value(caught && message == "boom" && sum.IsNumber() && sum.As<Napi::Number>().Int32Value() == 2);
});

EXPECT_TRUE(outcome.get_future().get());
}
matthargett added a commit to rebeckerspecialties/JsRuntimeHost that referenced this pull request Sep 17, 2026
…alue

Native code throws Napi::Error, a C++ exception carrying the JavaScript
error object. JSI reports any std::exception that escapes a host
function as a fresh Error("Exception in HostFunction: " + what()), so
on the JSI backend a polyfill's TypeError reached scripts as a plain
Error with a prefixed message: `instanceof TypeError` was false and
messages no longer compared equal, unlike every other backend.

Route every host-function trampoline (Function::New callbacks, class
constructors, static and instance methods and accessors) through a
helper that catches Napi::Error and rethrows jsi::JSError with the
original value, the same conversion Napi::Eval already does in the
other direction (BabylonJS#246).

Regression test: "native exceptions reach scripts as the thrown error
object, with its class and message, on every engine".

(cherry picked from commit 94e20fc)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants