Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

JSError.ThrowError(Exception) aborts the process when a Worker terminates during a .NET call

Ouverte
#505 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

@GalaxiasKyklos y travaille déjà.

Depuis le 13/10/2026.

  • #506 par @GalaxiasKyklos — ouverte

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
18/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
csharp, nodejs
Domaine
backend

Piste de recherche

Start with the static JSError.ThrowError(Exception) overload and its use in JSValue.InvokeCallback, then trace CreateErrorValueForException and JSError.Value to see where HasOwnProperty fails during termination. Run the minimal Worker repro to confirm the abort. Done means the hosted-CLR regression test exits cleanly. Linked PR #506 is already open against this issue, so check it before starting.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Summary

Calling worker.terminate() while a Node.js Worker is executing a .NET call through
node-api-dotnet reliably aborts the entire process with an unhandled
Microsoft.JavaScript.NodeApi.JSException.

The static JSError.ThrowError(Exception) callback error path is not safe when the Node-API
environment is terminating. It attempts to inspect or create JavaScript error values after V8 has
started terminating the Worker. Those Node-API operations return napi_pending_exception, causing
another JSException to escape the native callback boundary. CoreCLR then terminates the process
with SIGABRT (exit code 134).

This reproduces on node-api-dotnet 0.9.27 and is still present on the current main branch.

Environment

Component Version
node-api-dotnet 0.9.27
Node.js 24.13.0
.NET SDK/runtime 10.0.302 / Microsoft.NETCore.App 10.0.10
OS Ubuntu 24.04.4 LTS, glibc 2.39, x64
Container mcr.microsoft.com/dotnet/sdk:10.0

The package includes the worker teardown fixes from #487, #492, and #495.

Minimal reproduction

minimal-worker.cjs:

'use strict';
const dotnet = require('node-api-dotnet/net10.0');
const { parentPort } = require('worker_threads');

parentPort.postMessage('ready');
for (;;) {
  dotnet.System.String.Join(',', ['a', 'b', 'c']);
}

minimal.cjs:

'use strict';
const { Worker } = require('worker_threads');

(async () => {
  for (let i = 0; i < 200; i++) {
    const worker = new Worker(`${__dirname}/minimal-worker.cjs`);
    await new Promise((resolve) => worker.once('message', resolve));
    await new Promise((resolve) => setTimeout(resolve, 15));
    await worker.terminate();
  }
  console.log('ALL_CLEAN');
})();

Run:

node minimal.cjs

Observed on the first iteration:

Cannot print exception string because Exception.ToString() failed.
Aborted (core dumped)

Exit code: 134 / SIGABRT.

The delay can be varied from 0 through 200 ms without changing the result.

Controls

Worker state during teardown Result
Pure JavaScript; node-api-dotnet never loaded Clean exit
node-api-dotnet loaded, but no .NET call in flight Clean exit
.NET call in flight SIGABRT / exit 134

The same abort occurs with process.exit(0) while a Worker has a .NET call in flight, so this is
not specific to Worker.terminate().

Managed stack

An UnhandledException handler that avoids the overridden JSException.StackTrace property
captures:

Microsoft.JavaScript.NodeApi.JSException
An exception is pending: Error in HasOwnProperty at .../JSValue.cs:570

at Microsoft.JavaScript.NodeApi.JSValue.HasOwnProperty(JSValue key)
at Microsoft.JavaScript.NodeApi.JSError.get_Value()
at Microsoft.JavaScript.NodeApi.JSError.CreateErrorValueForException(Exception exception, ...)
at Microsoft.JavaScript.NodeApi.JSError.ThrowError(Exception exception)
at Microsoft.JavaScript.NodeApi.JSValue.InvokeCallback[TDescriptor](...)
at Microsoft.JavaScript.NodeApi.JSValue.InvokeJSMethod(...)

Under gdb, the process receives SIGABRT from CoreCLR's unhandled-managed-exception policy. This is
not a segmentation fault in the minimal reproduction.

Root cause

The callback path is:

  1. A JS-to-.NET call is executing in JSValue.InvokeCallback.
  2. Worker/environment termination begins.
  3. The managed callback fails and InvokeCallback catches the exception.
  4. The catch block calls static JSError.ThrowError(Exception).
  5. ThrowError(Exception) calls CreateErrorValueForException before checking whether the
    environment already has a pending exception or is closing.
  6. For a JSException, CreateErrorValueForException evaluates Error.Value.
  7. JSError.Value calls HasOwnProperty, which returns napi_pending_exception during teardown.
  8. ThrowIfFailed creates and throws another JSException.
  9. That exception escapes the unmanaged callback boundary and the process aborts.

CreateErrorValueForException also performs additional Node-API operations for
Error.captureStackTrace and DefineProperties, which can fail if termination starts after an
initial state check.

The instance JSError.ThrowError() already handles the equivalent condition:

if (status == napi_status.napi_pending_exception)
{
    // The environment must be terminating...
    return;
}

The static overload does not provide the same protection before constructing the JavaScript error.

Proposed fix

Make static JSError.ThrowError(Exception) termination-safe:

  1. Obtain the current runtime and environment.
  2. Call the raw IsExceptionPending runtime API before constructing a JavaScript error.
  3. Return if an exception is pending, or if the status is napi_pending_exception or
    napi_closing.
  4. If termination starts while constructing or augmenting the JavaScript Error, catch the
    resulting JSException and fall back to the direct runtime ThrowError(env, code, message)
    API, which does not inspect existing JavaScript error objects.
  5. Treat napi_pending_exception and napi_closing from the final throw as expected teardown
    outcomes.

A regression test can make the race deterministic by:

  1. Calling a managed method in a Worker.
  2. Having the managed method call back into JS to signal that it started.
  3. Blocking briefly in managed code and then throwing.
  4. Terminating the Worker from the parent after receiving the started signal.

Without the fix, the child process aborts. With the proposed fix, the hosted-CLR test exits cleanly.

Langage dominant
C#
Étoiles
787
Forks
80
Métriques de merge des PR
Aucune PR mergée en 30 j

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de microsoft/node-api-dotnet

Toutes les issues de microsoft/node-api-dotnet

Issues similaires

Plus d'issues C#

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.