Offload functions to worker threads with shared memory primitives for Node.js.
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: surface main-thread call site in error stack traces

Wrap errors in an async catch block inside trackValue (pool) and
runOnDedicated (dedicated), re-throwing with the worker error as
cause. V8's async stack traces then extend the new error with the
caller's await chain, so users see where run(), exec(), or await
task was called.

Adds one microtask hop per dispatch. The error subclass identity
and cause chain are preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+117 -21
+20
.changeset/error-stack-traces.md
··· 1 + --- 2 + 'moroutine': minor 3 + --- 4 + 5 + Include main-thread call site in error stack traces 6 + 7 + Errors thrown during task dispatch now have stack traces that show where `run()`, `exec()`, or `await task` was called. The original worker-side error is preserved as `err.cause` with its own stack pointing to the moroutine source. 8 + 9 + ``` 10 + Error: boom 11 + at trackValue (worker-pool.ts:52:15) 12 + at async loadUser (user-code.ts:7:3) 13 + at async main (user-code.ts:11:3) { 14 + [cause]: Error: boom 15 + at fixtures/math.ts:6:9 // original throw site on the worker 16 + at MessagePort.<anonymous> (worker-entry.ts:173:25) 17 + } 18 + ``` 19 + 20 + Built-in error subclass identity (`TypeError`, `RangeError`, etc.) is preserved on the outer wrapper.
+12 -2
src/dedicated-runner.ts
··· 28 28 if (count === 0) worker.unref(); 29 29 } 30 30 31 - export function runOnDedicated<T>(id: string, args: unknown[]): Promise<T> { 31 + export async function runOnDedicated<T>(id: string, args: unknown[]): Promise<T> { 32 32 const worker = getWorker(id); 33 33 ref(worker); 34 - return execute<T>(worker, id, args).finally(() => unref(worker)); 34 + try { 35 + return await execute<T>(worker, id, args); 36 + } catch (err) { 37 + if (err instanceof Error) { 38 + const Ctor = err.constructor as ErrorConstructor; 39 + throw new Ctor(err.message, { cause: err }); 40 + } 41 + throw new Error(String(err), { cause: err }); 42 + } finally { 43 + unref(worker); 44 + } 35 45 } 36 46 37 47 export function runStreamOnDedicated<T>(id: string, args: unknown[]): AsyncIterable<T> {
+26 -12
src/worker-pool.ts
··· 41 41 const inflight = new Set<Promise<unknown>>(); 42 42 const activeCounts = new Map<WorkerHandle, number>(); 43 43 44 - function track<T>(handle: WorkerHandle, promise: Promise<T>): Promise<T> { 44 + async function trackValue<T>(handle: WorkerHandle, promise: Promise<T>): Promise<T> { 45 45 inflight.add(promise); 46 46 activeCounts.set(handle, (activeCounts.get(handle) ?? 0) + 1); 47 - promise 48 - .finally(() => { 49 - inflight.delete(promise); 50 - activeCounts.set(handle, (activeCounts.get(handle) ?? 1) - 1); 51 - }) 52 - .catch(() => {}); 53 - return promise; 47 + try { 48 + return await promise; 49 + } catch (err) { 50 + if (err instanceof Error) { 51 + const Ctor = err.constructor as ErrorConstructor; 52 + throw new Ctor(err.message, { cause: err }); 53 + } 54 + throw new Error(String(err), { cause: err }); 55 + } finally { 56 + inflight.delete(promise); 57 + activeCounts.set(handle, (activeCounts.get(handle) ?? 1) - 1); 58 + } 59 + } 60 + 61 + function trackStream(handle: WorkerHandle, done: Promise<void>): void { 62 + inflight.add(done); 63 + activeCounts.set(handle, (activeCounts.get(handle) ?? 0) + 1); 64 + done.then(() => { 65 + inflight.delete(done); 66 + activeCounts.set(handle, (activeCounts.get(handle) ?? 1) - 1); 67 + }); 54 68 } 55 69 56 70 function terminateAll(): void { ··· 89 103 function dispatch<T>(task: Task): Promise<T> { 90 104 if (disposed) return Promise.reject(new Error('Worker pool is disposed')); 91 105 const { worker, handle } = resolveWorkerAndHandle(task); 92 - return track(handle, execute<T>(worker, task.id, task.args)); 106 + return trackValue(handle, execute<T>(worker, task.id, task.args)); 93 107 } 94 108 95 109 function makeWorkerHandle(worker: Worker, idx: number): WorkerHandle { ··· 99 113 if (task instanceof AsyncIterableTask) { 100 114 if (disposed) throw new Error('Worker pool is disposed'); 101 115 const { iterable, done } = dispatchStream(worker, task.id, task.args, channelOpts); 102 - track(handle, done); 116 + trackStream(handle, done); 103 117 return iterable; 104 118 } 105 119 if (disposed) return Promise.reject(new Error('Worker pool is disposed')); 106 - return track(handle, execute<T>(worker, task.id, task.args)); 120 + return trackValue(handle, execute<T>(worker, task.id, task.args)); 107 121 }, 108 122 get thread() { 109 123 return worker; ··· 123 137 if (disposed) throw new Error('Worker pool is disposed'); 124 138 const { worker, handle } = resolveWorkerAndHandle(taskOrTasks); 125 139 const { iterable, done } = dispatchStream(worker, taskOrTasks.id, taskOrTasks.args, channelOpts); 126 - track(handle, done); 140 + trackStream(handle, done); 127 141 return iterable; 128 142 } 129 143 if (Array.isArray(taskOrTasks)) {
+59 -7
test/error.test.ts
··· 18 18 }); 19 19 }); 20 20 21 - it('preserves stack trace pointing to worker source', async () => { 21 + it('preserves stack trace pointing to worker source via cause', async () => { 22 22 await using run = workers(1); 23 23 try { 24 24 await run(fail('stack check')); 25 25 assert.fail('should have thrown'); 26 26 } catch (err) { 27 27 assert.ok(err instanceof Error); 28 - assert.match(err.stack!, /fixtures\/math\.ts/); 28 + const cause = err.cause as Error; 29 + assert.ok(cause instanceof Error); 30 + assert.match(cause.stack!, /fixtures\/math\.ts/); 29 31 } 30 32 }); 31 33 ··· 36 38 assert.fail('should have thrown'); 37 39 } catch (err) { 38 40 assert.ok(err instanceof TypeError); 39 - assert.equal(err.message, 'type check'); 41 + assert.equal((err as Error).message, 'type check'); 40 42 } 41 43 }); 42 44 43 - it('preserves error cause', async () => { 45 + it('preserves error cause (nested through wrapper)', async () => { 44 46 await using run = workers(1); 45 47 try { 46 48 await run(failCause('with cause')); 47 49 assert.fail('should have thrown'); 48 50 } catch (err) { 49 51 assert.ok(err instanceof Error); 50 - assert.ok(err.cause instanceof RangeError); 51 - assert.equal((err.cause as Error).message, 'root cause'); 52 + // outer wrapper has the worker error as cause; worker error has RangeError as its cause 53 + const workerErr = (err as Error).cause as Error; 54 + assert.ok(workerErr instanceof Error); 55 + assert.ok(workerErr.cause instanceof RangeError); 56 + assert.equal((workerErr.cause as Error).message, 'root cause'); 52 57 } 53 58 }); 54 59 ··· 58 63 assert.fail('should have thrown'); 59 64 } catch (err) { 60 65 assert.ok(err instanceof TypeError); 61 - assert.match((err as Error).stack!, /fixtures\/math\.ts/); 66 + const cause = (err as Error).cause as Error; 67 + assert.match(cause.stack!, /fixtures\/math\.ts/); 68 + } 69 + }); 70 + 71 + it('main-thread stack includes run() caller', async () => { 72 + await using run = workers(1); 73 + 74 + async function someCaller() { 75 + await run(fail('trace check')); 76 + } 77 + 78 + try { 79 + await someCaller(); 80 + assert.fail('should have thrown'); 81 + } catch (err) { 82 + assert.ok(err instanceof Error); 83 + assert.match((err as Error).stack!, /someCaller/); 84 + } 85 + }); 86 + 87 + it('main-thread stack includes exec() caller', async () => { 88 + await using run = workers(1); 89 + 90 + async function someCaller() { 91 + await run.workers[0].exec(fail('exec trace check')); 92 + } 93 + 94 + try { 95 + await someCaller(); 96 + assert.fail('should have thrown'); 97 + } catch (err) { 98 + assert.ok(err instanceof Error); 99 + assert.match((err as Error).stack!, /someCaller/); 100 + } 101 + }); 102 + 103 + it('main-thread stack includes await-task caller on dedicated worker', async () => { 104 + async function someCaller() { 105 + await fail('dedicated trace check'); 106 + } 107 + 108 + try { 109 + await someCaller(); 110 + assert.fail('should have thrown'); 111 + } catch (err) { 112 + assert.ok(err instanceof Error); 113 + assert.match((err as Error).stack!, /someCaller/); 62 114 } 63 115 }); 64 116