QuickJS: report success from the napi_throw family - #225
Merged
bkaradzic-microsoft merged 1 commit intoAug 20, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR aligns the QuickJS Node-API (napi_*) throw APIs with upstream Node-API behavior by reporting success (napi_ok) after scheduling a JavaScript throw, preventing node-addon-api from taking an error-handling path that discards the real JS exception and can lead to use-after-free when building a fallback error message.
Changes:
- Update QuickJS
napi_throw*functions to clear last error and returnnapi_okafter throwing, matching upstream semantics. - Add a regression test ensuring native-thrown errors preserve their original JS type and exact message (no “Uncaught C++ exception …” fallback).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| Tests/UnitTests/Scripts/tests.ts | Adds a regression assertion for preserved error type/message when native code throws. |
| Core/Node-API/Source/js_native_api_quickjs.cc | Makes napi_throw, napi_throw_error, napi_throw_type_error, and napi_throw_range_error report success after scheduling the exception. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
napi_throw, napi_throw_error, napi_throw_type_error and napi_throw_range_error returned napi_pending_exception after successfully scheduling the throw. That status means "the call failed because an exception is already pending", so node-addon-api's Error::ThrowAsJavaScriptException treated every native throw as a failed throw and re-threw Error::New(env). That constructor consumes the pending exception via napi_get_and_clear_last_exception, so the C++ exception escaped WrapCallback with no JS exception set. ExternalCallback::Callback then took its fallback path and rebuilt the error from e.what(). By that point the handle scope opened by ThrowAsJavaScriptException had closed, so stringifying the message read freed memory: on Linux this segfaults in js_dup, and elsewhere it silently replaces the error with "InternalError: Uncaught C++ exception: ...". Return napi_ok instead, matching the upstream Node-API implementation, so the exception stays pending and propagates unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
bkaradzic-microsoft
force-pushed
the
quickjs-throw-status
branch
from
August 20, 2026 19:00
2e8b459 to
bc3ae52
Compare
bkaradzic-microsoft
pushed a commit
to bkaradzic-microsoft/BabylonNative
that referenced
this pull request
Aug 20, 2026
BabylonJS/JsRuntimeHost#225 makes the QuickJS napi_throw family report success, so a native throw no longer escapes the callback wrapper and get rebuilt from a freed error. Without it, the addPath() arity check added here segfaults the QuickJS unit tests on Linux. Also picks up BabylonJS#223. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
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.
Problem
napi_throw,napi_throw_error,napi_throw_type_errorandnapi_throw_range_errorreturnednapi_pending_exceptionafter successfully scheduling the throw.In Node-API that status means "this call failed because an exception was already pending", not "a throw is now pending". The upstream implementation returns
napi_clear_last_error(env)(i.e.napi_ok).Because the QuickJS port reported failure,
Error::ThrowAsJavaScriptExceptioninnapi-inl.htook its failure branch on every native throw:Error::New(env)callsnapi_get_and_clear_last_exception, so the pending JS exception is discarded and a fresh C++ exception is thrown out ofdetails::WrapCallback.ExternalCallback::Callbackthen catches it, observes!JS_HasException(ctx), and rebuilds the error frome.what().By that point the
HandleScopeopened byThrowAsJavaScriptExceptionhas been destroyed during unwinding, so stringifying the message reads freed memory.Impact
Two symptoms, both of which reproduce today:
InternalError: Uncaught C++ exception: <message>. Every native throw on QuickJS is affected, soerr.nameanderr instanceof TypeErrorare wrong throughout.The
JSValuebeing stringified carriesJS_TAG_STRINGwith an unaligned, freed pointer.I instrumented the
catchinExternalCallback::Callbackin a BabylonNative QuickJS build and confirmed that all ~50 native throws in that test run escapedWrapCallbackwithhasExc=0. After this change the count is 0.Fix
Return
napi_okfrom the four throw entry points, matching upstream. The exception stays pending,WrapCallbackreturns normally, and the fragilee.what()fallback is never entered.Test
Added a strict assertion to the existing
URLSearchParams.set()arity throw, checking the error type and exact message rather than a substring. The pre-existing.to.throw()test could not catch this, because"Uncaught C++ exception: <msg>"still contains the expected substring.Verified on Linux QuickJS (RelWithDebInfo):
expected 'InternalError' to equal 'Error'— 212 passing, 1 failingAlso verified in a BabylonNative QuickJS build on Windows: 21/21 gtest, 49 JS assertions, exit 0, and zero escapes from
WrapCallback.