Skip to content

Fatal crash: "Cannot create a handle without a HandleScope" when WebGPU is used from Vitest worker threads #13

@eliemichel

Description

@eliemichel

Hello, this module works nice overall, but I'm having trouble when using it in unit tests.

The webgpu native addon crashes with a V8/NAPI fatal error when requestAdapter() within a Vitest runner, where each test file runs in a worker thread. The crash happens when the promise returned by requestAdapter() resolves. The addon's async completion callback runs, and GPUAdapter::Bind (or similar) attempts to create an EscapableHandleScope but fails.

Steps to reproduce

  1. Create a Vitest project with the webgpu package as a dependency.
  2. In vitest.setup.js, inject navigator.gpu via create([]) from webgpu.
  3. Write a test that calls await navigator.gpu.requestAdapter() (or createGpuContext() which uses it) from within an it() test callback.
  4. Run vitest run.

NB: The crash only occurs when calling requestAdapter from within the test body. It does not occur when the same call is made from a beforeAll hook in the same worker (which is thus a possible workaround).

Example of error trace:

 5: 0x10228c04c napi_open_escapable_handle_scope
 6: 0x127dec98c Napi::EscapableHandleScope::EscapableHandleScope(Napi::Env)
 7: 0x127dec7e8 Napi::FunctionReference::New(...)
 8: 0x127e2d0ac wgpu::interop::GPUAdapter::Bind(...)
 9: 0x127df0b08 wgpu::interop::Interface<...>::Create<...>
10: 0x127df0810 wgpu::binding::GPU::requestAdapter(...)
...
18: 0x1054b1290 Builtins_AsyncFunctionAwaitResolveClosure
19: 0x10557c4d8 Builtins_PromiseFulfillReactionJob
20: 0x1054a1594 Builtins_RunMicrotasks
...
34: 0x104769a70 node::InternalCallbackScope::Close
35: 0x104769cac node::InternalMakeCallback
36: 0x104780288 node::AsyncWrap::MakeCallback
37: 0x104980b74 node::StreamBase::CallJSOnreadMethod
38: 0x104982308 node::EmitToJSStreamListener::OnStreamRead

Tested environment:

  • Package: webgpu@0.3.8
  • Node.js: v22.19.0 (also observed on Node 20.x)
  • OS: macOS (darwin)
  • Test runner: Vitest 3.2.4 (uses worker threads via tinypool)

My Agent's 2 cents

I'm not familiar with the Node addon mechanism, so here is what my AI agent thinks of this issue FWIW:

Root cause (hypothesis)

The crash occurs when the addon's async completion runs in a callback context where V8's HandleScope is not properly set up. The stack trace shows the promise resolution happens during StreamBase::CallJSOnreadMethod—i.e., when Node is processing a stream read (likely from worker stdout/IPC). In that nested callback context, napi_open_escapable_handle_scope fails because the required HandleScope invariant is not satisfied.

Suggested fix

Ensure the addon's async completion callbacks always establish a proper HandleScope before creating any NAPI handles. For example, at the start of the callback that runs when requestAdapter or similar completes, the native code should call napi_open_handle_scope (or equivalent) before creating EscapableHandleScope or any other handles. This would make the addon robust to being invoked from arbitrary callback contexts (worker threads, stream callbacks, etc.).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions