Skip to content

Commit 2900f9b

Browse files
authored
Merge pull request #13 from MatrixAI/feature-stack-trace
WIP: Resetting the stack trace
2 parents eb4278e + b07e554 commit 2900f9b

13 files changed

+241
-27
lines changed

docs/interfaces/createDestroy.CreateDestroy.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/interfaces/createDestroyStartStop.CreateDestroyStartStop.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/interfaces/startStop.StartStop.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/modules/createDestroy.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/modules/createDestroyStartStop.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/modules/startStop.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/CreateDestroy.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
AsyncFunction,
1010
GeneratorFunction,
1111
AsyncGeneratorFunction,
12+
resetStackTrace,
1213
} from './utils';
1314
import { ErrorAsyncInitDestroyed } from './errors';
1415

@@ -97,14 +98,14 @@ function ready(
9798
if (block) {
9899
return this[initLock].withReadF(async () => {
99100
if (this[_destroyed]) {
100-
errorDestroyed.stack = new Error().stack;
101+
resetStackTrace(errorDestroyed, descriptor[kind]);
101102
throw errorDestroyed;
102103
}
103104
return f.apply(this, args);
104105
});
105106
} else {
106107
if (this[initLock].isLocked('write') || this[_destroyed]) {
107-
errorDestroyed.stack = new Error().stack;
108+
resetStackTrace(errorDestroyed, descriptor[kind]);
108109
throw errorDestroyed;
109110
}
110111
return f.apply(this, args);
@@ -116,7 +117,7 @@ function ready(
116117
return yield* f.apply(this, args);
117118
}
118119
if (this[initLock].isLocked('write') || this[_destroyed]) {
119-
errorDestroyed.stack = new Error().stack;
120+
resetStackTrace(errorDestroyed, descriptor[kind]);
120121
throw errorDestroyed;
121122
}
122123
return yield* f.apply(this, args);
@@ -129,14 +130,14 @@ function ready(
129130
if (block) {
130131
return yield* this[initLock].withReadG(() => {
131132
if (this[_destroyed]) {
132-
errorDestroyed.stack = new Error().stack;
133+
resetStackTrace(errorDestroyed, descriptor[kind]);
133134
throw errorDestroyed;
134135
}
135136
return f.apply(this, args);
136137
});
137138
} else {
138139
if (this[initLock].isLocked('write') || this[_destroyed]) {
139-
errorDestroyed.stack = new Error().stack;
140+
resetStackTrace(errorDestroyed, descriptor[kind]);
140141
throw errorDestroyed;
141142
}
142143
return yield* f.apply(this, args);
@@ -148,7 +149,7 @@ function ready(
148149
return f.apply(this, args);
149150
}
150151
if (this[initLock].isLocked('write') || this[_destroyed]) {
151-
errorDestroyed.stack = new Error().stack;
152+
resetStackTrace(errorDestroyed, descriptor[kind]);
152153
throw errorDestroyed;
153154
}
154155
return f.apply(this, args);

src/CreateDestroyStartStop.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AsyncFunction,
1212
GeneratorFunction,
1313
AsyncGeneratorFunction,
14+
resetStackTrace,
1415
} from './utils';
1516
import {
1617
ErrorAsyncInitRunning,
@@ -77,7 +78,8 @@ function CreateDestroyStartStop<
7778
return;
7879
}
7980
if (this[_running]) {
80-
errorRunning.stack = new Error().stack ?? '';
81+
// Unfortunately `this.destroy` doesn't work as the decorated function
82+
resetStackTrace(errorRunning);
8183
throw errorRunning;
8284
}
8385
let result;
@@ -100,7 +102,8 @@ function CreateDestroyStartStop<
100102
return;
101103
}
102104
if (this[_destroyed]) {
103-
errorDestroyed.stack = new Error().stack ?? '';
105+
// Unfortunately `this.start` doesn't work as the decorated function
106+
resetStackTrace(errorDestroyed);
104107
throw errorDestroyed;
105108
}
106109
let result;
@@ -125,7 +128,8 @@ function CreateDestroyStartStop<
125128
if (this[_destroyed]) {
126129
// It is not possible to be running and destroyed
127130
// however this line is here for completion
128-
errorDestroyed.stack = new Error().stack ?? '';
131+
// Unfortunately `this.stop` doesn't work as the decorated function
132+
resetStackTrace(errorDestroyed);
129133
throw errorDestroyed;
130134
}
131135
let result;
@@ -176,14 +180,14 @@ function ready(
176180
if (block) {
177181
return this[initLock].withReadF(async () => {
178182
if (!this[_running]) {
179-
errorNotRunning.stack = new Error().stack;
183+
resetStackTrace(errorNotRunning, descriptor[kind]);
180184
throw errorNotRunning;
181185
}
182186
return f.apply(this, args);
183187
});
184188
} else {
185189
if (this[initLock].isLocked('write') || !this[_running]) {
186-
errorNotRunning.stack = new Error().stack;
190+
resetStackTrace(errorNotRunning, descriptor[kind]);
187191
throw errorNotRunning;
188192
}
189193
return f.apply(this, args);
@@ -195,7 +199,7 @@ function ready(
195199
return yield* f.apply(this, args);
196200
}
197201
if (this[initLock].isLocked('write') || !this[_running]) {
198-
errorNotRunning.stack = new Error().stack;
202+
resetStackTrace(errorNotRunning, descriptor[kind]);
199203
throw errorNotRunning;
200204
}
201205
return yield* f.apply(this, args);
@@ -208,14 +212,14 @@ function ready(
208212
if (block) {
209213
return yield* this[initLock].withReadG(() => {
210214
if (!this[_running]) {
211-
errorNotRunning.stack = new Error().stack;
215+
resetStackTrace(errorNotRunning, descriptor[kind]);
212216
throw errorNotRunning;
213217
}
214218
return f.apply(this, args);
215219
});
216220
} else {
217221
if (this[initLock].isLocked('write') || !this[_running]) {
218-
errorNotRunning.stack = new Error().stack;
222+
resetStackTrace(errorNotRunning, descriptor[kind]);
219223
throw errorNotRunning;
220224
}
221225
return yield* f.apply(this, args);
@@ -227,7 +231,7 @@ function ready(
227231
return f.apply(this, args);
228232
}
229233
if (this[initLock].isLocked('write') || !this[_running]) {
230-
errorNotRunning.stack = new Error().stack;
234+
resetStackTrace(errorNotRunning, descriptor[kind]);
231235
throw errorNotRunning;
232236
}
233237
return f.apply(this, args);

src/StartStop.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
AsyncFunction,
1010
GeneratorFunction,
1111
AsyncGeneratorFunction,
12+
resetStackTrace,
1213
} from './utils';
1314
import { ErrorAsyncInitNotRunning } from './errors';
1415

@@ -118,14 +119,14 @@ function ready(
118119
if (block) {
119120
return this[initLock].withReadF(async () => {
120121
if (!this[_running]) {
121-
errorNotRunning.stack = new Error().stack;
122+
resetStackTrace(errorNotRunning, descriptor[kind]);
122123
throw errorNotRunning;
123124
}
124125
return f.apply(this, args);
125126
});
126127
} else {
127128
if (this[initLock].isLocked('write') || !this[_running]) {
128-
errorNotRunning.stack = new Error().stack;
129+
resetStackTrace(errorNotRunning, descriptor[kind]);
129130
throw errorNotRunning;
130131
}
131132
return f.apply(this, args);
@@ -137,7 +138,7 @@ function ready(
137138
return yield* f.apply(this, args);
138139
}
139140
if (this[initLock].isLocked('write') || !this[_running]) {
140-
errorNotRunning.stack = new Error().stack;
141+
resetStackTrace(errorNotRunning, descriptor[kind]);
141142
throw errorNotRunning;
142143
}
143144
return yield* f.apply(this, args);
@@ -150,14 +151,14 @@ function ready(
150151
if (block) {
151152
return yield* this[initLock].withReadG(() => {
152153
if (!this[_running]) {
153-
errorNotRunning.stack = new Error().stack;
154+
resetStackTrace(errorNotRunning, descriptor[kind]);
154155
throw errorNotRunning;
155156
}
156157
return f.apply(this, args);
157158
});
158159
} else {
159160
if (this[initLock].isLocked('write') || !this[_running]) {
160-
errorNotRunning.stack = new Error().stack;
161+
resetStackTrace(errorNotRunning, descriptor[kind]);
161162
throw errorNotRunning;
162163
}
163164
return yield* f.apply(this, args);
@@ -169,7 +170,7 @@ function ready(
169170
return f.apply(this, args);
170171
}
171172
if (this[initLock].isLocked('write') || !this[_running]) {
172-
errorNotRunning.stack = new Error().stack;
173+
resetStackTrace(errorNotRunning, descriptor[kind]);
173174
throw errorNotRunning;
174175
}
175176
return f.apply(this, args);

src/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,31 @@ const AsyncFunction = (async () => {}).constructor;
1313
const GeneratorFunction = function* () {}.constructor;
1414
const AsyncGeneratorFunction = async function* () {}.constructor;
1515

16+
const hasCaptureStackTrace = 'captureStackTrace' in Error;
17+
18+
/**
19+
* Ready wrappers take exception objects
20+
* JS exception traces are created when the exception is instantiated
21+
* This function rewrites the stack trace according to where the wrapped
22+
* function is called, giving a more useful stack trace
23+
*/
24+
// eslint-disable-next-line @typescript-eslint/ban-types
25+
function resetStackTrace(error: Error, decorated?: Function): void {
26+
if (error.stack != null) {
27+
const stackTitle = error.stack.slice(0, error.stack.indexOf('\n') + 1);
28+
if (hasCaptureStackTrace) {
29+
// Only available on v8
30+
// This will start the trace where the decorated function is called
31+
Error.captureStackTrace(error, decorated);
32+
} else {
33+
// Non-V8 systems have to do with just a normal stack
34+
// it is bit more noisy
35+
error.stack = new Error().stack ?? '';
36+
}
37+
error.stack = error.stack.replace(/[^\n]+\n/, stackTitle);
38+
}
39+
}
40+
1641
export {
1742
_running,
1843
running,
@@ -24,4 +49,6 @@ export {
2449
AsyncFunction,
2550
GeneratorFunction,
2651
AsyncGeneratorFunction,
52+
hasCaptureStackTrace,
53+
resetStackTrace,
2754
};

tests/CreateDestroy.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,60 @@ describe('CreateDestroy', () => {
191191
expect(x.f()).toBe('X');
192192
expect(x.prop).toStrictEqual(['X']);
193193
});
194+
test('exception name is preserved', async () => {
195+
interface X extends CreateDestroy {}
196+
@CreateDestroy()
197+
class X {
198+
@ready()
199+
public doSomethingSync() {}
200+
201+
@ready(new Error())
202+
public async doSomethingAsync() {}
203+
204+
@ready(new ReferenceError('foo'))
205+
public *doSomethingGenSync() {}
206+
207+
@ready(new TypeError('abc'))
208+
public async *doSomethingGenAsync() {}
209+
}
210+
const x = new X();
211+
await x.destroy();
212+
let e;
213+
try {
214+
x.doSomethingSync();
215+
} catch (e_) {
216+
e = e_;
217+
}
218+
expect(e.name).toBe(ErrorAsyncInitDestroyed.name);
219+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
220+
`${e.name}: ${e.message}\n`,
221+
);
222+
try {
223+
await x.doSomethingAsync();
224+
} catch (e_) {
225+
e = e_;
226+
}
227+
expect(e.name).toBe('Error');
228+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(`Error: \n`);
229+
try {
230+
x.doSomethingGenSync().next();
231+
} catch (e_) {
232+
e = e_;
233+
}
234+
expect(e.name).toBe('ReferenceError');
235+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
236+
`ReferenceError: foo\n`,
237+
);
238+
try {
239+
await x.doSomethingGenAsync().next();
240+
} catch (e_) {
241+
e = e_;
242+
}
243+
expect(e.name).toBe('TypeError');
244+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
245+
`TypeError: abc\n`,
246+
);
247+
});
194248
test('symbols do not conflict with existing properties', async () => {
195249
interface X extends CreateDestroy {}
196250
@CreateDestroy()

tests/CreateDestroyStartStop.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,80 @@ describe('CreateDestroyStartStop', () => {
295295
expect(x.f()).toBe('X');
296296
expect(x.prop).toStrictEqual(['X']);
297297
});
298+
test('exception name is preserved', async () => {
299+
interface X extends CreateDestroyStartStop {}
300+
@CreateDestroyStartStop(new RangeError(), new SyntaxError())
301+
class X {
302+
@ready()
303+
public doSomethingSync() {}
304+
305+
@ready(new Error())
306+
public async doSomethingAsync() {}
307+
308+
@ready(new ReferenceError('foo'))
309+
public *doSomethingGenSync() {}
310+
311+
@ready(new TypeError('abc'))
312+
public async *doSomethingGenAsync() {}
313+
}
314+
const x = new X();
315+
let e;
316+
try {
317+
x.doSomethingSync();
318+
} catch (e_) {
319+
e = e_;
320+
}
321+
expect(e.name).toBe(ErrorAsyncInitNotRunning.name);
322+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
323+
`${e.name}: ${e.message}\n`,
324+
);
325+
try {
326+
await x.doSomethingAsync();
327+
} catch (e_) {
328+
e = e_;
329+
}
330+
expect(e.name).toBe('Error');
331+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(`Error: \n`);
332+
try {
333+
x.doSomethingGenSync().next();
334+
} catch (e_) {
335+
e = e_;
336+
}
337+
expect(e.name).toBe('ReferenceError');
338+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
339+
`ReferenceError: foo\n`,
340+
);
341+
try {
342+
await x.doSomethingGenAsync().next();
343+
} catch (e_) {
344+
e = e_;
345+
}
346+
expect(e.name).toBe('TypeError');
347+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
348+
`TypeError: abc\n`,
349+
);
350+
await x.start();
351+
try {
352+
await (async () => {
353+
await x.destroy();
354+
})();
355+
} catch (e_) {
356+
e = e_;
357+
}
358+
expect(e.name).toBe('RangeError');
359+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(`RangeError: \n`);
360+
await x.stop();
361+
await x.destroy();
362+
try {
363+
await x.start();
364+
} catch (e_) {
365+
e = e_;
366+
}
367+
expect(e.name).toBe('SyntaxError');
368+
expect(e.stack!.slice(0, e.stack.indexOf('\n') + 1)).toBe(
369+
`SyntaxError: \n`,
370+
);
371+
});
298372
test('symbols do not conflict with existing properties', async () => {
299373
interface X extends CreateDestroyStartStop {}
300374
@CreateDestroyStartStop()

0 commit comments

Comments
 (0)