1import type {
2 AsyncResourceFiber,
3 AsyncResourceNode,
4} from './asyncResourceGraph';
5
6export type FiberErrorCode =
7 | 'FOREIGN_ASYNC_TRIGGER'
8 | 'PARENT_ASYNC_TRIGGER'
9 | 'FOREIGN_ASYNC_ABORTED'
10 | 'FIBER_ABORTED'
11 | 'FIBER_STALL';
12
13const codeToMessage = (
14 code: FiberErrorCode,
15 fiber: AsyncResourceFiber,
16 node: AsyncResourceNode
17): string => {
18 switch (code) {
19 case 'FOREIGN_ASYNC_TRIGGER':
20 return (
21 `${fiber} tried to create ${node} which will be triggered by async IO from a different fiber.\n` +
22 'Fibers are isolated and may only create and reference async resources they have created themselves.'
23 );
24 case 'PARENT_ASYNC_TRIGGER':
25 return (
26 `${fiber} tried to create ${node} which will be triggered by async IO of this fiber's parent context.\n` +
27 'Fibers are isolated and may only create and reference async resources they have created themselves.'
28 );
29 case 'FOREIGN_ASYNC_ABORTED':
30 return (
31 `${fiber} used ${node} from another fiber which was aborted and can never resolve.\n` +
32 'Fibers may not share async resources, and may accidentally prevent each other from resolving if they do.'
33 );
34 case 'FIBER_ABORTED':
35 return (
36 `${fiber}'s ${node} was aborted and will never resolve.\n` +
37 "If you see this message, you're observing an internal forceful cancellation of a fiber and this error is expected."
38 );
39 case 'FIBER_STALL':
40 return (
41 `${fiber} has finished all of its work but won't resolve and pass control back to the parent fiber.\n` +
42 'This usally happens if a Promise is unresolved or if its async IO has been cancelled without a callback being handled.\n' +
43 `${node} is the last async resource the fiber got stuck on.`
44 );
45 }
46};
47
48const traceNode = (
49 node: AsyncResourceNode,
50 fiber: AsyncResourceFiber,
51 depth = 1
52): string => {
53 let trace = `${node}`;
54 let origin: AsyncResourceNode | null = node;
55 for (let idx = 1; origin && origin !== fiber.root && idx <= depth; idx++) {
56 if (origin.frame) trace += `\n at ${origin.frame}`;
57 origin = origin.executionOrigin;
58 }
59 return trace;
60};
61
62export class FiberError extends Error {
63 static stackTraceLimit = 10;
64
65 readonly fiber: AsyncResourceFiber;
66 readonly node: AsyncResourceNode;
67 readonly code: FiberErrorCode;
68
69 constructor(
70 code: FiberErrorCode,
71 fiber: AsyncResourceFiber,
72 node: AsyncResourceNode
73 ) {
74 super(codeToMessage(code, fiber, node));
75 this.fiber = fiber;
76 this.node = node;
77 this.code = code;
78 }
79
80 get trace(): string {
81 let trace = traceNode(this.node, this.fiber);
82 if (this.node.triggerOrigin)
83 trace += `\ntriggered by ${traceNode(this.node.triggerOrigin, this.fiber, FiberError.stackTraceLimit)}`;
84 if (this.node.executionOrigin)
85 trace += `\nexecuted in ${traceNode(this.node.executionOrigin, this.fiber, FiberError.stackTraceLimit)}`;
86 return trace;
87 }
88
89 toString() {
90 return `${this.message.trim()}\n\n${this.trace}`;
91 }
92}