diff --git a/packages/zone.js/lib/common/promise.ts b/packages/zone.js/lib/common/promise.ts index 2e12b15c53f8a..d588e1d52088d 100644 --- a/packages/zone.js/lib/common/promise.ts +++ b/packages/zone.js/lib/common/promise.ts @@ -20,6 +20,8 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr const __symbol__ = api.symbol; const _uncaughtPromiseErrors: UncaughtPromiseError[] = []; + const isDisableWrappingUncaughtPromiseRejection = + global[__symbol__('DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION')] === true; const symbolPromise = __symbol__('Promise'); const symbolThen = __symbol__('then'); const creationTrace = '__creationTrace__'; @@ -41,13 +43,11 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr api.microtaskDrainDone = () => { while (_uncaughtPromiseErrors.length) { - while (_uncaughtPromiseErrors.length) { - const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift() !; - try { - uncaughtPromiseError.zone.runGuarded(() => { throw uncaughtPromiseError; }); - } catch (error) { - handleUnhandledRejection(error); - } + const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift() !; + try { + uncaughtPromiseError.zone.runGuarded(() => { throw uncaughtPromiseError; }); + } catch (error) { + handleUnhandledRejection(error); } } }; @@ -58,7 +58,7 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr api.onUnhandledError(e); try { const handler = (Zone as any)[UNHANDLED_PROMISE_REJECTION_HANDLER_SYMBOL]; - if (handler && typeof handler === 'function') { + if (typeof handler === 'function') { handler.call(this, e); } } catch (err) { @@ -176,20 +176,28 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr } if (queue.length == 0 && state == REJECTED) { (promise as any)[symbolState] = REJECTED_NO_CATCH; - try { - // try to print more readable error log - throw new Error( - 'Uncaught (in promise): ' + readableObjectToString(value) + - (value && value.stack ? '\n' + value.stack : '')); - } catch (err) { - const error: UncaughtPromiseError = err; - error.rejection = value; - error.promise = promise; - error.zone = Zone.current; - error.task = Zone.currentTask !; - _uncaughtPromiseErrors.push(error); - api.scheduleMicroTask(); // to make sure that it is running + let uncaughtPromiseError = value; + if (!isDisableWrappingUncaughtPromiseRejection) { + // If disable wrapping uncaught promise reject + // and the rejected value is an Error object, + // use the value instead of wrapping it. + try { + // Here we throws a new Error to print more readable error log + // and if the value is not an error, zone.js builds an `Error` + // Object here to attach the stack information. + throw new Error( + 'Uncaught (in promise): ' + readableObjectToString(value) + + (value && value.stack ? '\n' + value.stack : '')); + } catch (err) { + uncaughtPromiseError = err; + } } + uncaughtPromiseError.rejection = value; + uncaughtPromiseError.promise = promise; + uncaughtPromiseError.zone = Zone.current; + uncaughtPromiseError.task = Zone.currentTask !; + _uncaughtPromiseErrors.push(uncaughtPromiseError); + api.scheduleMicroTask(); // to make sure that it is running } } } diff --git a/packages/zone.js/lib/zone.configurations.api.ts b/packages/zone.js/lib/zone.configurations.api.ts index 4eaa4bb94bab1..88629890eff8a 100644 --- a/packages/zone.js/lib/zone.configurations.api.ts +++ b/packages/zone.js/lib/zone.configurations.api.ts @@ -529,6 +529,17 @@ interface ZoneGlobalConfigurations { * The preceding code makes all scroll event listeners passive. */ __zone_symbol__PASSIVE_EVENTS?: boolean; + + /** + * Disable wrapping uncaught promise rejection. + * + * By default, `zone.js` wraps the uncaught promise rejection in a new `Error` object + * which contains additional information such as a value of the rejection and a stack trace. + * + * If you set `__zone_symbol__DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION = true;` before + * importing `zone.js`, `zone.js` will not wrap the uncaught promise rejection. + */ + __zone_symbol__DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION?: boolean; } /** diff --git a/packages/zone.js/test/BUILD.bazel b/packages/zone.js/test/BUILD.bazel index c00dff042221b..57b17eea764fe 100644 --- a/packages/zone.js/test/BUILD.bazel +++ b/packages/zone.js/test/BUILD.bazel @@ -33,6 +33,7 @@ ts_library( ], exclude = [ "common/Error.spec.ts", + "common/promise-disable-wrap-uncaught-promise-rejection.spec.ts", ], ), deps = [ @@ -253,7 +254,10 @@ env_entry_point = ":browser-env-setup.ts" test_srcs = glob( ["browser/*.ts"], - exclude = ["browser/shadydom.spec.ts"], + exclude = [ + "browser/shadydom.spec.ts", + "common/promise-disable-wrap-uncaught-promise-rejection.spec.ts", + ], ) + [ "extra/cordova.spec.ts", "mocha-patch.spec.ts", @@ -323,3 +327,24 @@ karma_test( "browser_shadydom_entry_point.ts", ], ) + +karma_test( + name = "browser_disable_wrap_uncaught_promise_rejection", + bootstraps = {"browser_disable_wrap_uncaught_promise_rejection": [ + "//packages/zone.js/dist:zone-testing-bundle.js", + ]}, + ci = False, + env_deps = [ + "//packages/zone.js/lib", + ], + env_entry_point = ":browser_disable_wrap_uncaught_promise_rejection_setup.ts", + env_srcs = ["browser_disable_wrap_uncaught_promise_rejection_setup.ts"], + test_deps = [ + "//packages/zone.js/lib", + ], + test_entry_point = ":browser_disable_wrap_uncaught_promise_rejection_entry_point.ts", + test_srcs = [ + "common/promise-disable-wrap-uncaught-promise-rejection.spec.ts", + "browser_disable_wrap_uncaught_promise_rejection_entry_point.ts", + ], +) diff --git a/packages/zone.js/test/browser_disable_wrap_uncaught_promise_rejection_entry_point.ts b/packages/zone.js/test/browser_disable_wrap_uncaught_promise_rejection_entry_point.ts new file mode 100644 index 0000000000000..3480c04f83333 --- /dev/null +++ b/packages/zone.js/test/browser_disable_wrap_uncaught_promise_rejection_entry_point.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import './common/promise-disable-wrap-uncaught-promise-rejection.spec'; diff --git a/packages/zone.js/test/browser_disable_wrap_uncaught_promise_rejection_setup.ts b/packages/zone.js/test/browser_disable_wrap_uncaught_promise_rejection_setup.ts new file mode 100644 index 0000000000000..7f19c73fb9ad3 --- /dev/null +++ b/packages/zone.js/test/browser_disable_wrap_uncaught_promise_rejection_setup.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +(window as any)['__zone_symbol__DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION'] = true; diff --git a/packages/zone.js/test/common/promise-disable-wrap-uncaught-promise-rejection.spec.ts b/packages/zone.js/test/common/promise-disable-wrap-uncaught-promise-rejection.spec.ts new file mode 100644 index 0000000000000..7e07317015827 --- /dev/null +++ b/packages/zone.js/test/common/promise-disable-wrap-uncaught-promise-rejection.spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +class TestRejection { + prop1?: string; + prop2?: string; +} + +describe('disable wrap uncaught promise rejection', () => { + it('should notify Zone.onHandleError if promise is uncaught', (done) => { + let promiseError: Error|null = null; + let zone: Zone|null = null; + let task: Task|null = null; + let error: Error|null = null; + Zone.current + .fork({ + name: 'promise-error', + onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any): + boolean => { + promiseError = error; + delegate.handleError(target, error); + return false; + } + }) + .run(() => { + zone = Zone.current; + task = Zone.currentTask; + error = new Error('rejectedErrorShouldBeHandled'); + try { + // throw so that the stack trace is captured + throw error; + } catch (e) { + } + Promise.reject(error); + expect(promiseError).toBe(null); + }); + setTimeout((): any => null); + setTimeout(() => { + expect(promiseError).toBe(error); + expect((promiseError as any)['rejection']).toBe(error); + expect((promiseError as any)['zone']).toBe(zone); + expect((promiseError as any)['task']).toBe(task); + done(); + }); + }); + + it('should print original information when a non-Error object is used for rejection', (done) => { + let promiseError: Error|null = null; + let rejectObj: TestRejection; + Zone.current + .fork({ + name: 'promise-error', + onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any): + boolean => { + promiseError = error; + delegate.handleError(target, error); + return false; + } + }) + .run(() => { + rejectObj = new TestRejection(); + rejectObj.prop1 = 'value1'; + rejectObj.prop2 = 'value2'; + (rejectObj as any).message = 'rejectMessage'; + Promise.reject(rejectObj); + expect(promiseError).toBe(null); + }); + setTimeout((): any => null); + setTimeout(() => { + expect(promiseError).toEqual(rejectObj as any); + done(); + }); + }); +});