Skip to content

build: define NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS to stop teardown aborts - #954

Open
Amir (amirlehmam) wants to merge 1 commit into
microsoft:mainfrom
amirlehmam:fix/swallow-unthrowable-exceptions
Open

build: define NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS to stop teardown aborts#954
Amir (amirlehmam) wants to merge 1 commit into
microsoft:mainfrom
amirlehmam:fix/swallow-unthrowable-exceptions

Conversation

@amirlehmam

Copy link
Copy Markdown

Fixes the root cause in #951 (same signature as #904).

The problem

Error::ThrowAsJavaScriptException() in node-addon-api ends with:

#ifdef NAPI_CPP_EXCEPTIONS
    if (status != napi_ok) {
      throw Error::New(_env);
    }
#endif

In the ConPTY exit path that throw has no handler above it. The
ExitEvent callback in src/win/conpty.cc runs on the main thread from
the ThreadSafeFunction dispatcher — unlike every other throw Napi::Error::New(...) in that file, it is not inside a function
registered as an N-API method, so nothing converts it into a JS throw.
It reaches UnhandledExceptionFilter and the host process is aborted
(0xc0000409, FAST_FAIL_FATAL_APP_EXIT).

This only happens while the environment is terminating. NAPI_PREAMBLE
returns napi_pending_exception when !(last_exception.IsEmpty() && can_call_into_js()), while napi_is_exception_pending reports only
!last_exception.IsEmpty(). So a napi call failing with
napi_pending_exception and napi_is_exception_pending returning
false means can_call_into_js() == false — nothing is wrong with the
addon's code, the isolate is simply going away.

The fix

node-addon-api already handles this, and its own comment describes the
situation precisely:

      if (status == napi_pending_exception) {
        // The environment must be terminating as we checked earlier and there
        // was no pending exception. In this case continuing will result
        // in a fatal error and there is nothing the author has done incorrectly
        // in their code that is worth flagging through a fatal error
        return;
      }

That guard is behind NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS. node-pty
depends on node_addon_api_except, and except.gypi defines
NAPI_CPP_EXCEPTIONS and nothing else — so the #else branch is what
goes into the shipped prebuilds. Defining the macro in target_defaults
compiles the guard in.

There are two guarded sites in node-addon-api 7.1.1, not one:
ThrowAsJavaScriptException (napi-inl.h:3042) and the Error(napi_env, napi_value) constructor's napi_define_properties call
(napi-inl.h:2924), which otherwise hits NAPI_FATAL_IF_FAILED. Both
are abort paths during teardown; both close with this one define.

The define only changes behaviour when the environment is already
terminating — an exception that can be thrown still is.

Verification

Built on Windows with VS 2022 BuildTools, Node 24.13.0, node-gyp 11.4.2:

  • npx node-gyp rebuild completes (gyp info ok), producing
    conpty.node and conpty_console_list.node.

  • The define appears in the generated projects next to
    NAPI_CPP_EXCEPTIONS, which is the pairing that matters:

    PreprocessorDefinitions>NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS;NODE_GYP_MODULE_NAME=conpty;…;NAPI_CPP_EXCEPTIONS;_HAS_EXCEPTIONS=1;…
    
  • No new warnings; the only ones on conpty.cc are the pre-existing
    C6387s at lines 414 and 448.

Two caveats about that build, stated rather than glossed: I disabled
SpectreMitigation locally to compile, because this machine lacks the
Spectre-mitigated libraries (MSB8040) — unmodified main fails the
same way on it, so that is environmental and is not part of this
diff. And the pty (winpty) target was not generated because
deps/winpty was not checked out. The affected target is conpty, and
it built.

I do not have a machine that reproduces the crash on demand, so I cannot
offer a before/after soak. The reporter in #951 has thirteen crash dumps
with a byte-identical signature and can test a patched build.

An N-API call that fails with napi_pending_exception while the environment
is tearing down currently reaches `throw Error::New(_env)` in
Error::ThrowAsJavaScriptException, which has no handler above it in the
ThreadSafeFunction dispatch path and terminates the host process.

node-addon-api ships a guard for exactly this case, but only behind
NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS. node-pty depends on
node_addon_api_except, whose except.gypi defines NAPI_CPP_EXCEPTIONS and
nothing else, so the throwing path is what gets compiled into the
prebuilds.

Refs microsoft#951, microsoft#904.
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.

1 participant