Workers
Contents
- The pattern
- Errors
- Memory across the thread boundary
- Returning JS values directly
- When not to use a worker
env.runWorker offloads CPU work to a background thread and returns a JS Promise. It is the right tool for single-result async work: parsing, hashing, image processing, anything that takes long enough to block the main thread.
For multi-call async patterns (progress events, streaming), use a ThreadsafeFn instead.
The pattern
Define a struct with two methods:
compute(*Self) voidruns on the worker thread. No JS access here. Treat it like any other Zig function.resolve(*Self, Env) !Truns on the main thread. The return value (or error) becomes the promise result.Tmay be any convertible Zig type,napi.Val, orvoid.
The first argument to runWorker is a name shown in the Node async hooks API. The second is the context struct that will be copied to the heap and passed to compute and resolve.
Errors
If resolve returns an error, the Promise rejects with a real JS Error whose .message is the Zig error name:
compute itself does not return a value, so to surface a "computation failed" outcome, store state on self and check it from resolve:
A panic in compute (index out of bounds, unreachable, integer overflow) crashes the entire Node.js process. There is no way to recover from a panic on a worker thread. Use error returns for anything that can fail.
Memory across the thread boundary
The worker context is copied to the heap before runWorker returns. Anything you put in it must outlive the function that called runWorker. Arena memory (strings from JS, allocations from env.allocator()) will be dangling by the time compute runs.
Copy what you need first:
The convention is for compute or resolve to free the long-lived allocation when it is done with it. The bridge frees the context wrapper itself.
Returning JS values directly
If you want resolve to return a hand-built napi.Val instead of a typed Zig value (for Buffers, dynamic-key objects, anything outside the conversion table):
Val is a recognized return type. The bridge passes it through unconverted.
When not to use a worker
- Synchronous work that completes in microseconds. The thread hop has its own cost. Just compute on the main thread.
- Multi-call patterns. Progress events, streaming, anything where the worker pushes more than one value. Use ThreadsafeFn.
- I/O-bound work. If the work spends its time waiting for the kernel, use Node's existing async I/O instead of pinning a thread.