Threadsafe functions
Contents
- The pattern
- Reference counting
- The payload type
- Call modes
- Keeping the event loop alive
- Method summary
- Signal-only callbacks
ThreadsafeFn(T) lets a background thread call back into JS. Node is single-threaded, so you cannot call N-API from a spawned thread directly. ThreadsafeFn queues calls back to the main thread and runs them there.
Use it for multi-call patterns: progress events, streaming results, pub/sub. For single-result background work, use Workers instead.
The pattern
- Create a
ThreadsafeFnfrom anapi.Callbackwithcb.threadsafe(env, name, T), or configure a bound withthreadsafeWithOptions. - Hand the
ThreadsafeFnto whatever spawned threads will call into JS. - Each thread calls
acquire()to register itself, then callscall(value, mode)whenever it wants to invoke the JS callback. - Each thread calls
release()when it is done.
Reference counting
ThreadsafeFn lives as long as it has at least one reference. The reference count starts at 1 (held by the main thread that created it). Every additional thread that wants to call must:
acquire()before it starts, to register itself.release()when it stops, to drop its reference.
When the count drops to zero, the wrapper is destroyed and the JS callback is unrooted. Forgetting to release keeps the JS function alive forever; double-release is a use-after-free.
The standard recipe (as in the example above) is defer ts.release() immediately after a successful acquire().
The payload type
The third argument to cb.threadsafe(env, name, T) is the per-call payload. The bridge converts each T to a JS value before invoking the callback.
For struct payloads, the field-by-field walk applies as usual:
For slice-backed payloads (including strings and slices nested in structs, arrays, optionals, or tagged unions), call queues a deep snapshot. Ordinary pointers to fixed arrays are snapshotted too. The calling thread may mutate or release the original data after call returns.
Raw single-item pointers, many-item/sentinel pointers, C pointers, specially qualified pointers, recursively self-referential slice graphs, and containers that include them are rejected at compile time because the queue cannot infer a safe length and ownership model. Copy their data into a value, slice, or fixed array before calling call; send an ID instead of an opaque external resource.
napi.Env, napi.Val, napi.Callback, napi.CallInfo, and containers that include them are also rejected at compile time. They contain call-scoped Node-API handles, not transferable data, and cannot outlive the callback or cross a thread. Convert the JavaScript value into native Zig-owned data before enqueueing it. Persistent APIs such as napi.Ref still have their own documented main-thread and release requirements and are not ordinary payload data.
If payload conversion fails or the JavaScript callback throws, the bridge forwards that error exactly once through Node.js's uncaughtException path. A threadsafe call has no per-call Promise or return channel, so applications should install the error policy appropriate for their process.
Call modes
call(value, mode) takes a napi.ThreadsafeFn(T).Mode:
.blockingwaits if a bounded queue is full. It can deadlock if the main thread is blocked too..non_blockingreturnserror.QueueFullimmediately if a bounded queue is full. Drop the event or retry, your choice.
cb.threadsafe(env, name, T) is unbounded for compatibility, so neither mode encounters a full queue. For high-rate streams, configure a positive bound and use .non_blocking plus an explicit drop or retry policy:
ThreadsafeOptions.max_queue_size = 0 selects the unbounded default.
Keeping the event loop alive
A ThreadsafeFn keeps the Node.js event loop alive by default. The process will not exit while a ThreadsafeFn exists. Use unref(env) to detach:
This is the same semantic as setInterval(..).unref() in Node. Use it when the threadsafe function is a watchdog or telemetry channel that should not by itself prevent shutdown.
ref(env) reverses it.
Method summary
Signal-only callbacks
Use void as the payload type when the JS callback takes no arguments: