Offload functions to worker threads with shared memory primitives for Node.js.
1import { describe, it } from 'node:test';
2import assert from 'node:assert/strict';
3import { workers } from 'moroutine';
4import { fail, failType, failCause } from './fixtures/math.ts';
5import { failAfterType } from './fixtures/stream-gen.ts';
6
7describe('error handling', () => {
8 it('rejects with error from dedicated worker', async () => {
9 await assert.rejects(async () => fail('boom').then((x) => x), {
10 message: 'boom',
11 });
12 });
13
14 it('rejects with error from pool worker', async () => {
15 await using run = workers(1);
16 await assert.rejects(() => run(fail('pool boom')), {
17 message: 'pool boom',
18 });
19 });
20
21 it('preserves stack trace pointing to worker source via cause', async () => {
22 await using run = workers(1);
23 try {
24 await run(fail('stack check'));
25 assert.fail('should have thrown');
26 } catch (err) {
27 assert.ok(err instanceof Error);
28 const cause = err.cause as Error;
29 assert.ok(cause instanceof Error);
30 assert.match(cause.stack!, /fixtures\/math\.ts/);
31 }
32 });
33
34 it('preserves built-in error subclass identity', async () => {
35 await using run = workers(1);
36 try {
37 await run(failType('type check'));
38 assert.fail('should have thrown');
39 } catch (err) {
40 assert.ok(err instanceof TypeError);
41 assert.equal((err as Error).message, 'type check');
42 }
43 });
44
45 it('preserves error cause (nested through wrapper)', async () => {
46 await using run = workers(1);
47 try {
48 await run(failCause('with cause'));
49 assert.fail('should have thrown');
50 } catch (err) {
51 assert.ok(err instanceof Error);
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');
57 }
58 });
59
60 it('preserves error details on dedicated worker', async () => {
61 try {
62 await failType('dedicated type');
63 assert.fail('should have thrown');
64 } catch (err) {
65 assert.ok(err instanceof TypeError);
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/);
114 }
115 });
116
117 it('preserves error details on streaming task', async () => {
118 await using run = workers(1);
119 try {
120 for await (const _ of run(failAfterType(2))) {
121 // consume yields until error
122 }
123 assert.fail('should have thrown');
124 } catch (err) {
125 assert.ok(err instanceof TypeError);
126 assert.equal((err as Error).message, 'stream type error');
127 assert.match((err as Error).stack!, /fixtures\/stream-gen\.ts/);
128 }
129 });
130});