Skip to content

Commit fa75800

Browse files
atscottalxhub
authored andcommitted
fix(zone.js): avoid type error on custom object rejection with rejection property
Ensure that when a custom object with a 'rejection' property is thrown as a raw promise rejection, the unhandled promise rejection error logger does not crash with a TypeError while trying to access undefined zone properties. Also wrap microtask queue draining and task frame counter updates with defensive try-finally blocks to guarantee internal scheduler states are properly reset under any potential call stack exception unwinding scenarios.
1 parent 31399c2 commit fa75800

3 files changed

Lines changed: 52 additions & 20 deletions

File tree

‎packages/zone.js/lib/common/promise.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function patchPromise(Zone: ZoneType): void {
3434
api.onUnhandledError = (e: any) => {
3535
if (api.showUncaughtError()) {
3636
const rejection = e && e.rejection;
37-
if (rejection) {
37+
if (rejection && e.zone && e.task) {
3838
console.error(
3939
'Unhandled Promise rejection:',
4040
rejection instanceof Error ? rejection.message : rejection,

‎packages/zone.js/lib/zone-impl.ts‎

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,10 +1435,13 @@ export function initZone(): ZoneType {
14351435
task.runCount++;
14361436
return task.zone.runTask(task, target, args);
14371437
} finally {
1438-
if (_numberOfNestedTaskFrames === 1 && !global[enableNativeMicrotaskDraining]) {
1439-
drainMicroTaskQueueSynchronously();
1438+
try {
1439+
if (_numberOfNestedTaskFrames === 1 && !global[enableNativeMicrotaskDraining]) {
1440+
drainMicroTaskQueueSynchronously();
1441+
}
1442+
} finally {
1443+
_numberOfNestedTaskFrames--;
14401444
}
1441-
_numberOfNestedTaskFrames--;
14421445
}
14431446
}
14441447

@@ -1551,27 +1554,32 @@ export function initZone(): ZoneType {
15511554

15521555
_isDrainingMicrotaskQueue = true;
15531556

1554-
while (_microTaskQueue.length) {
1555-
const queue = _microTaskQueue;
1556-
_microTaskQueue = [];
1557+
try {
1558+
while (_microTaskQueue.length) {
1559+
const queue = _microTaskQueue;
1560+
_microTaskQueue = [];
15571561

1558-
for (const task of queue) {
1562+
for (const task of queue) {
1563+
try {
1564+
task.zone.runTask(task, null, null);
1565+
} catch (error) {
1566+
_api.onUnhandledError(error as Error);
1567+
}
1568+
}
1569+
}
1570+
} finally {
1571+
// The order matters!
1572+
if (global[enableNativeMicrotaskDraining]) {
1573+
_isDrainingMicrotaskQueue = false;
1574+
_api.microtaskDrainDone();
1575+
} else {
15591576
try {
1560-
task.zone.runTask(task, null, null);
1561-
} catch (error) {
1562-
_api.onUnhandledError(error as Error);
1577+
_api.microtaskDrainDone();
1578+
} finally {
1579+
_isDrainingMicrotaskQueue = false;
15631580
}
15641581
}
15651582
}
1566-
1567-
// The order matters!
1568-
if (global[enableNativeMicrotaskDraining]) {
1569-
_isDrainingMicrotaskQueue = false;
1570-
_api.microtaskDrainDone();
1571-
} else {
1572-
_api.microtaskDrainDone();
1573-
_isDrainingMicrotaskQueue = false;
1574-
}
15751583
}
15761584

15771585
//////////////////////////////////////////////////////

‎packages/zone.js/test/common/promise-disable-wrap-uncaught-promise-rejection.spec.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,28 @@ describe('disable wrap uncaught promise rejection', () => {
110110
done();
111111
});
112112
});
113+
114+
it('should handle a custom object rejection with a rejection property without crashing the error logger', async () => {
115+
await jasmine.spyOnGlobalErrorsAsync(() => {
116+
const originalConsoleError = console.error;
117+
console.error = jasmine.createSpy('consoleErr');
118+
119+
const rejectObj = {
120+
rejection: 'custom-inner-rejection',
121+
message: 'custom-error-message',
122+
};
123+
124+
Zone.current.fork({name: 'promise-error-zone'}).run(() => {
125+
Promise.reject(rejectObj);
126+
});
127+
128+
return new Promise<void>((res) => {
129+
setTimeout(() => {
130+
expect(console.error).toHaveBeenCalledWith(rejectObj);
131+
console.error = originalConsoleError;
132+
res();
133+
});
134+
});
135+
});
136+
});
113137
});

0 commit comments

Comments
 (0)