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

Commit f3547cc

Browse files
JiaLiPassionmhevery
authored andcommitted
feat(promise): fix #621, add unhandledRejection handler and ignore consoleError (#627)
1 parent 47962df commit f3547cc

File tree

7 files changed

+183
-7
lines changed

7 files changed

+183
-7
lines changed

Diff for: lib/browser/browser.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {patchTimer} from '../common/timers';
10-
import {patchClass, patchMethod, patchPrototype, zoneSymbol} from '../common/utils';
10+
import {findEventTask, patchClass, patchMethod, patchPrototype, zoneSymbol} from '../common/utils';
1111

1212
import {propertyPatch} from './define-property';
1313
import {eventTargetPatch} from './event-target';
@@ -142,3 +142,26 @@ function patchXHR(window: any) {
142142
if (_global['navigator'] && _global['navigator'].geolocation) {
143143
patchPrototype(_global['navigator'].geolocation, ['getCurrentPosition', 'watchPosition']);
144144
}
145+
146+
// handle unhandled promise rejection
147+
function findPromiseRejectionHandler(evtName: string) {
148+
return function(e: any) {
149+
const eventTask = findEventTask(_global, evtName);
150+
if (eventTask) {
151+
// windows has added unhandledrejection event listener
152+
// trigger the event listener
153+
const PromiseRejectionEvent = _global['PromiseRejectionEvent'];
154+
if (PromiseRejectionEvent) {
155+
const evt = new PromiseRejectionEvent(evtName, {promise: e.promise, reason: e.rejection});
156+
eventTask.invoke(evt);
157+
}
158+
}
159+
};
160+
}
161+
162+
if (_global['PromiseRejectionEvent']) {
163+
Zone[zoneSymbol('unhandledPromiseRejectionHandler')] =
164+
findPromiseRejectionHandler('unhandledrejection');
165+
166+
Zone[zoneSymbol('rejectionHandledHandler')] = findPromiseRejectionHandler('rejectionhandled');
167+
}

Diff for: lib/common/utils.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ export interface MicroTaskMeta extends TaskData {
527527
callbackIndex: number;
528528
args: any[];
529529
}
530+
530531
export function patchMicroTask(
531532
obj: any, funcName: string, metaCreator: (self: any, args: any[]) => MicroTaskMeta) {
532533
let setNative = null;
@@ -551,4 +552,19 @@ export function patchMicroTask(
551552
return delegate.apply(self, args);
552553
}
553554
});
554-
}
555+
}
556+
557+
export function findEventTask(target: any, evtName: string) {
558+
const eventTasks: Task[] = target[zoneSymbol('eventTasks')];
559+
if (eventTasks) {
560+
for (let i = 0; i < eventTasks.length; i++) {
561+
const eventTask = eventTasks[i];
562+
const data = eventTask.data;
563+
const eventName = data && (<any>data).eventName;
564+
if (eventName === evtName) {
565+
return eventTask;
566+
}
567+
}
568+
}
569+
return undefined;
570+
}

Diff for: lib/node/node.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import './events';
1111
import './fs';
1212

1313
import {patchTimer} from '../common/timers';
14-
import {patchMacroTask, patchMethod, patchMicroTask} from '../common/utils';
14+
import {findEventTask, patchMacroTask, patchMicroTask, zoneSymbol} from '../common/utils';
1515

1616
const set = 'set';
1717
const clear = 'clear';
@@ -33,6 +33,7 @@ if (shouldPatchGlobalTimers) {
3333

3434
// patch process related methods
3535
patchProcess();
36+
handleUnhandledPromiseRejection();
3637

3738
// Crypto
3839
let crypto;
@@ -67,4 +68,28 @@ function patchProcess() {
6768
target: process
6869
};
6970
});
70-
}
71+
}
72+
73+
// handle unhandled promise rejection
74+
function findProcessPromiseRejectionHandler(evtName: string) {
75+
return function(e: any) {
76+
const eventTask = findEventTask(process, evtName);
77+
if (eventTask) {
78+
// process has added unhandledrejection event listener
79+
// trigger the event listener
80+
if (evtName === 'unhandledRejection') {
81+
eventTask.invoke(e.rejection, e.promise);
82+
} else if (evtName === 'rejectionHandled') {
83+
eventTask.invoke(e.promise);
84+
}
85+
}
86+
};
87+
}
88+
89+
function handleUnhandledPromiseRejection() {
90+
Zone[zoneSymbol('unhandledPromiseRejectionHandler')] =
91+
findProcessPromiseRejectionHandler('unhandledRejection');
92+
93+
Zone[zoneSymbol('rejectionHandledHandler')] =
94+
findProcessPromiseRejectionHandler('rejectionHandled');
95+
}

Diff for: lib/zone.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,9 @@ const Zone: ZoneType = (function(global: any) {
10411041
}
10421042

10431043
function consoleError(e: any) {
1044+
if (Zone[__symbol__('ignoreConsoleErrorUncaughtError')]) {
1045+
return;
1046+
}
10441047
const rejection = e && e.rejection;
10451048
if (rejection) {
10461049
console.error(
@@ -1052,6 +1055,17 @@ const Zone: ZoneType = (function(global: any) {
10521055
console.error(e);
10531056
}
10541057

1058+
function handleUnhandledRejection(e: any) {
1059+
consoleError(e);
1060+
try {
1061+
const handler = Zone[__symbol__('unhandledPromiseRejectionHandler')];
1062+
if (handler && typeof handler === 'function') {
1063+
handler.apply(this, [e]);
1064+
}
1065+
} catch (err) {
1066+
}
1067+
}
1068+
10551069
function drainMicroTaskQueue() {
10561070
if (!_isDrainingMicrotaskQueue) {
10571071
_isDrainingMicrotaskQueue = true;
@@ -1075,7 +1089,7 @@ const Zone: ZoneType = (function(global: any) {
10751089
throw uncaughtPromiseError;
10761090
});
10771091
} catch (error) {
1078-
consoleError(error);
1092+
handleUnhandledRejection(error);
10791093
}
10801094
}
10811095
}
@@ -1196,11 +1210,22 @@ const Zone: ZoneType = (function(global: any) {
11961210

11971211
function clearRejectedNoCatch(promise: ZoneAwarePromise<any>): void {
11981212
if (promise[symbolState] === REJECTED_NO_CATCH) {
1213+
// if the promise is rejected no catch status
1214+
// and queue.length > 0, means there is a error handler
1215+
// here to handle the rejected promise, we should trigger
1216+
// windows.rejectionhandled eventHandler or nodejs rejectionHandled
1217+
// eventHandler
1218+
try {
1219+
const handler = Zone[__symbol__('rejectionHandledHandler')];
1220+
if (handler && typeof handler === 'function') {
1221+
handler.apply(this, [{rejection: promise[symbolValue], promise: promise}]);
1222+
}
1223+
} catch (err) {
1224+
}
11991225
promise[symbolState] = REJECTED;
12001226
for (let i = 0; i < _uncaughtPromiseErrors.length; i++) {
12011227
if (promise === _uncaughtPromiseErrors[i].promise) {
12021228
_uncaughtPromiseErrors.splice(i, 1);
1203-
break;
12041229
}
12051230
}
12061231
}

Diff for: promise-adapter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require('./dist/zone-node.js');
2-
2+
Zone[('__zone_symbol__ignoreConsoleErrorUncaughtError')] = true;
33
module.exports.deferred = function() {
44
const p = {};
55
p.promise = new Promise((resolve, reject) => {

Diff for: test/browser/browser.spec.ts

+51
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {zoneSymbol} from '../../lib/common/utils';
910
import {ifEnvSupports} from '../test-util';
1011

1112
function windowPrototype() {
1213
return !!(global['Window'] && global['Window'].prototype);
1314
}
1415

16+
function promiseUnhandleRejectionSupport() {
17+
return !!global['PromiseRejectionEvent'];
18+
}
19+
1520
describe('Zone', function() {
1621
const rootZone = Zone.current;
1722

@@ -142,6 +147,52 @@ describe('Zone', function() {
142147
expect(button.onclick).not.toBe(null);
143148
});
144149
});
150+
151+
it('should support window.addEventListener(unhandledrejection)', function(done) {
152+
if (!promiseUnhandleRejectionSupport()) {
153+
done();
154+
return;
155+
}
156+
Zone[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
157+
rootZone.fork({name: 'promise'}).run(function() {
158+
const listener = (evt: any) => {
159+
expect(evt.type).toEqual('unhandledrejection');
160+
expect(evt.promise.constructor.name).toEqual('Promise');
161+
expect(evt.reason.message).toBe('promise error');
162+
window.removeEventListener('unhandledrejection', listener);
163+
done();
164+
};
165+
window.addEventListener('unhandledrejection', listener);
166+
new Promise((resolve, reject) => {
167+
throw new Error('promise error');
168+
});
169+
});
170+
});
171+
172+
it('should support window.addEventListener(rejectionhandled)', function(done) {
173+
if (!promiseUnhandleRejectionSupport()) {
174+
done();
175+
return;
176+
}
177+
Zone[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
178+
rootZone.fork({name: 'promise'}).run(function() {
179+
const listener = (evt: any) => {
180+
window.removeEventListener('unhandledrejection', listener);
181+
p.catch(reason => {});
182+
};
183+
window.addEventListener('unhandledrejection', listener);
184+
185+
window.addEventListener('rejectionhandled', (evt: any) => {
186+
expect(evt.type).toEqual('rejectionhandled');
187+
expect(evt.promise.constructor.name).toEqual('Promise');
188+
expect(evt.reason.message).toBe('promise error');
189+
done();
190+
});
191+
const p = new Promise((resolve, reject) => {
192+
throw new Error('promise error');
193+
});
194+
});
195+
});
145196
});
146197
});
147198
});

Diff for: test/node/process.spec.ts

+36
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {zoneSymbol} from '../../lib/common/utils';
10+
911
describe('process related test', () => {
1012
let zoneA, result;
1113
beforeEach(() => {
@@ -62,4 +64,38 @@ describe('process related test', () => {
6264
done();
6365
});
6466
});
67+
it('should support window.addEventListener(unhandledrejection)', function(done) {
68+
const hookSpy = jasmine.createSpy('hook');
69+
Zone[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
70+
Zone.current.fork({name: 'promise'}).run(function() {
71+
process.on('unhandledRejection', function(reason, promise) {
72+
hookSpy(promise, reason.message);
73+
});
74+
const p = new Promise((resolve, reject) => {
75+
throw new Error('promise error');
76+
});
77+
78+
setTimeout(function() {
79+
expect(hookSpy).toHaveBeenCalledWith(p, 'promise error');
80+
done();
81+
}, 10);
82+
});
83+
});
84+
85+
it('should support window.addEventListener(rejectionHandled)', function(done) {
86+
Zone[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true;
87+
Zone.current.fork({name: 'promise'}).run(function() {
88+
process.on('rejectionHandled', function(promise) {
89+
expect(promise).toEqual(p);
90+
done();
91+
});
92+
const p = new Promise((resolve, reject) => {
93+
throw new Error('promise error');
94+
});
95+
96+
setTimeout(function() {
97+
p.catch(reason => {});
98+
}, 10);
99+
});
100+
});
65101
});

0 commit comments

Comments
 (0)