Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 81297ee

Browse files
committed
fix: add support for subclassing of Errors
Calling `new Error` returns a new instance of `Error`. This means that subclassing of `Error` needs to take that into account as calling `super` will not expect return value. To fix this `ZoneAwareError` detects to see if it was called with `new` and if so will copy the fields from the `Error` to the current instance.
1 parent 6010557 commit 81297ee

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

Diff for: lib/zone.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1703,6 +1703,20 @@ const Zone: ZoneType = (function(global: any) {
17031703
// 1. attach zone information to stack frame
17041704
// 2. remove zone internal stack frames
17051705
attachZoneAndRemoveInternalZoneFrames(error);
1706+
if (this instanceof NativeError && this.constructor != NativeError) {
1707+
// We got called with a `new` operator AND we are subclass of ZoneAwareError
1708+
// in that case we have to copy all of our properties to `this`.
1709+
Object.keys(error).concat('stack', 'message').forEach((key) => {
1710+
if ((error as any)[key] !== undefined) {
1711+
try {
1712+
this[key] = (error as any)[key];
1713+
} catch (e) {
1714+
// ignore the assignment in case it is a setter and it throws.
1715+
}
1716+
}
1717+
});
1718+
return this;
1719+
}
17061720
return error;
17071721
}
17081722

Diff for: test/common/Error.spec.ts

+91-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,75 @@
99
const _global: any =
1010
typeof window === 'object' && window || typeof self === 'object' && self || global;
1111

12+
// simulate @angular/facade/src/error.ts
13+
class BaseError extends Error {
14+
/** @internal **/
15+
_nativeError: Error;
16+
17+
constructor(message: string) {
18+
super(message);
19+
const nativeError = new Error(message) as any as Error;
20+
this._nativeError = nativeError;
21+
}
22+
23+
get message() {
24+
return this._nativeError.message;
25+
}
26+
set message(message) {
27+
this._nativeError.message = message;
28+
}
29+
get name() {
30+
return this._nativeError.name;
31+
}
32+
get stack() {
33+
return (this._nativeError as any).stack;
34+
}
35+
set stack(value) {
36+
(this._nativeError as any).stack = value;
37+
}
38+
toString() {
39+
return this._nativeError.toString();
40+
}
41+
}
42+
43+
class WrappedError extends BaseError {
44+
originalError: any;
45+
46+
constructor(message: string, error: any) {
47+
super(`${message} caused by: ${error instanceof Error ? error.message : error}`);
48+
this.originalError = error;
49+
}
50+
51+
get stack() {
52+
return ((this.originalError instanceof Error ? this.originalError : this._nativeError) as any)
53+
.stack;
54+
}
55+
}
56+
57+
class TestError extends WrappedError {
58+
constructor(message: string, error: any) {
59+
super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error);
60+
}
61+
62+
get message() {
63+
return 'test ' + this.originalError.message;
64+
}
65+
}
66+
67+
class TestMessageError extends WrappedError {
68+
constructor(message: string, error: any) {
69+
super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error);
70+
}
71+
72+
get message() {
73+
return 'test ' + this.originalError.message;
74+
}
75+
76+
set message(value) {
77+
this.originalError.message = value;
78+
}
79+
}
80+
1281
describe('ZoneAwareError', () => {
1382
// If the environment does not supports stack rewrites, then these tests will fail
1483
// and there is no point in running them.
@@ -18,7 +87,7 @@ describe('ZoneAwareError', () => {
1887
class MyError extends Error {}
1988
const myError = new MyError();
2089
expect(myError instanceof Error).toBe(true);
21-
expect(myError instanceof _global[(Zone as any).__symbol__('Error')]).toBe(true);
90+
expect(myError instanceof MyError).toBe(true);
2291
expect(myError.stack).not.toBe(undefined);
2392
});
2493

@@ -70,6 +139,27 @@ describe('ZoneAwareError', () => {
70139
}
71140
});
72141

142+
it('should not use child Error class get/set in ZoneAwareError constructor', () => {
143+
const func = () => {
144+
const error = new BaseError('test');
145+
expect(error.message).toEqual('test');
146+
};
147+
148+
expect(func).not.toThrow();
149+
});
150+
151+
it('should behave correctly with wrapped error', () => {
152+
const error = new TestError('originalMessage', new Error('error message'));
153+
expect(error.message).toEqual('test error message');
154+
error.originalError.message = 'new error message';
155+
expect(error.message).toEqual('test new error message');
156+
157+
const error1 = new TestMessageError('originalMessage', new Error('error message'));
158+
expect(error1.message).toEqual('test error message');
159+
error1.message = 'new error message';
160+
expect(error1.message).toEqual('test new error message');
161+
});
162+
73163
it('should copy customized NativeError properties to ZoneAwareError', () => {
74164
const spy = jasmine.createSpy('errorCustomFunction');
75165
const NativeError = (global as any)[(Zone as any).__symbol__('Error')];

0 commit comments

Comments
 (0)