napi/v8: never leave a JS exception on the VM while addon code runs
What does this PR do?
A Node-API or V8-API call returns to the addon's C/C++ code, which cannot check a JS exception; the first frame that can is the trampoline the addon eventually returns to. This makes the addon boundary work the way Node's does (NAPI_PREAMBLE's TryCatch → env->last_exception → rethrown by CallIntoModule) and the way JSC's own C API does (APIUtils.h handleExceptionIfNeeded): an exception raised inside an API call is taken off the VM and latched on the napi_env, the call returns its status, and the trampoline that entered the addon throws it once the addon returns — NapiClass call/construct/getter/setter, process.dlopen (both napi_register_module_v1 and napi_module_register), finalizers, async-work completion, threadsafe-function call_js, and bun:ffi cc() symbols that take napi_env/napi_value. napi_throw* already worked like this; now every path does:
NAPI_RETURN_IF_EXCEPTION/NAPI_RETURN_STATUS_IF_EXCEPTION/NAPI_CHECK_TO_OBJECTand the Rust napi functions latch instead of returning with the exception still on the VM. A pending termination is the one thing left there — it is not the addon's to catch and it unwinds as soon as the addon returns.napi_is_exception_pending/napi_get_and_clear_last_exceptionread only the latch, as Node reads onlylast_exception(previously a VM termination read as "pending" but could never be cleared, which trips node-addon-api's swallow-on-shutdown heuristic).napi_run_scriptevaluates withoutJSC::evaluate's catch-and-return, so a throw is latched like any other (statusnapi_generic_failure, as Node) and a termination stays put. The value latched is what the script threw, not the engine'sExceptionwrapper.napi_call_functionno longer throws a latched exception into the VM before refusing the call; the preamble already returnsnapi_pending_exception.napi_create_dataview,napi_create_buffer[_copy],node_api_create_buffer_from_arraybuffer,napi_create_bigint_words,napi_run_scriptuseNAPI_PREAMBLElike the rest of the file (and like Node, so they now honour the can-call-into-JS gate during teardown);napi_create_dataviewover a detachedArrayBufferis aTypeErrorinstead of a crash;napi_get_all_property_nameschecks eachget/toPropertyKey/getOwnPropertyDescriptor/push.- The exception checks read the scope directly rather than through JSC's
RETURN_IF_EXCEPTION, so they no longer service VM traps: aworker.terminate()is observed at the addon's next JS entry / the can-call-into-JS gate rather than at an arbitrary napi call. bun:fficc():napi_throw_errorfrom compiled C used to crash; an API-raised exception there is now thrown when the symbol returns.- An addon that calls
napi_throw*from anInitregistered vianapi_module_registernow fails therequire()with that error (it was dropped). - V8 shim:
v8::Isolate::ThrowException/ThrowErrorlatch on the isolate and theFunctionTemplatecall/construct and accessor trampolines throw on return (v8::Function::Call/Object::Getetc. still report a JS throw through their emptyMaybewith it pending, as before).node_module_registerreports an init-time throw through the slotprocess.dlopenthrows from;process.dlopenclears its module/exports slots on every exit and throws whatever registration left there. JSValue::to_object(Rust) goes through the generatednull_is_throwwrapper so the validator sees its check.
How did you verify your code works?
New tests: napi_module_register + throwing Init; napi_create_dataview on a detached buffer (napi_pending_exception + TypeError); napi_run_script → napi_get_and_clear_last_exception yields the thrown TypeError / a thrown primitive unchanged (compared with Node; on current release napi_typeof rejects the value); v8::Isolate::ThrowException(42) followed by further API use before returning (compared with Node); cc() symbols that napi_throw_error / call a throwing JS function through napi_call_function (crash / lost on current release). Under BUN_JSC_validateExceptionChecks=1 on a debug build: test/napi/napi.test.ts 191/191, every test/napi/node-napi-tests js-native-api and node-api suite, test/v8/v8.test.ts 80/80, test/js/bun/ffi/cc.test.ts. node-addon-api (Error::New / NAPI_THROW_IF_FAILED / ObjectWrap), napi-rs (check_pending_exception! / throw_into) and neon error paths were traced against the new semantics and match Node's.