JSError.ThrowError(Exception) aborts the process when a Worker terminates during a .NET call
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 18/100
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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:
- A JS-to-.NET call is executing in
JSValue.InvokeCallback. - Worker/environment termination begins.
- The managed callback fails and
InvokeCallbackcatches the exception. - The catch block calls static
JSError.ThrowError(Exception). ThrowError(Exception)callsCreateErrorValueForExceptionbefore checking whether the
environment already has a pending exception or is closing.- For a
JSException,CreateErrorValueForExceptionevaluatesError.Value. JSError.ValuecallsHasOwnProperty, which returnsnapi_pending_exceptionduring teardown.ThrowIfFailedcreates and throws anotherJSException.- 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:
- Obtain the current runtime and environment.
- Call the raw
IsExceptionPendingruntime API before constructing a JavaScript error. - Return if an exception is pending, or if the status is
napi_pending_exceptionor
napi_closing. - If termination starts while constructing or augmenting the JavaScript
Error, catch the
resultingJSExceptionand fall back to the direct runtimeThrowError(env, code, message)
API, which does not inspect existing JavaScript error objects. - Treat
napi_pending_exceptionandnapi_closingfrom the final throw as expected teardown
outcomes.
A regression test can make the race deterministic by:
- Calling a managed method in a Worker.
- Having the managed method call back into JS to signal that it started.
- Blocking briefly in managed code and then throwing.
- 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.
- Lenguaje dominante
- C#
- Estrellas
- 787
- Forks
- 80
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de microsoft/node-api-dotnet
-
Dificultad 1/5 1-3 horas Aptitud para principiantes 88/100
microsoft/node-api-dotnet#484 ·
-
Embedding args occasionally get trailing garbage on .NET (missing null terminator on a pooled buffer)Posiblemente ocupada @tolmachev-pravo la tomó hace 92 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
microsoft/node-api-dotnet#481 ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 70/100
microsoft/node-api-dotnet#504 ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 82/100
microsoft/node-api-dotnet#502 ·
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
microsoft/node-api-dotnet#486 · 1 comentario ·
Todos los issues de microsoft/node-api-dotnet
Issues similares
-
documentation
Dificultad 1/5 1-3 horas Aptitud para principiantes 62/100
-
bug misleading tooling trivial
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
Docs example projects pinned to a literal library version instead of 3.*Posiblemente ocupada @DaveSkender la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
facioquo/stock-indicators-dotnet#2372 ·
Los mantenedores suelen responder en 1 día
-
[Docs] officecli-docx SKILL.md still describes `\n` as a soft line break after #262Posiblemente ocupada @giprime la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
Facepunch/sbox-public#12097 ·
Los mantenedores suelen responder en 2 días