Skip to content

Commit 65adb30

Browse files
atscottkirjs
authored andcommitted
feat(core): Add provider which reports unhandled errors on window to ErrorHandler (angular#60704)
This commit adds a provider that installs listeners on the browser window to forward unhandled promise rejections and uncaught errors to the `ErrorHandler`. This is useful for both ZoneJS and Zoneless applications. For apps using ZoneJS, errors can reach the window when they happen outside the Angular Zone. For Zoneless apps, any errors not explicitly caught by the framework can reach the window. Without this provider, these errors would otherwise not be reported to `ErrorHandler`. We will/should consider adding this provider to apps by default in the cli. In addition, it should be mentioned in the (to be created) documentation page on error handling in Angular. relates to angular#56240 PR Close angular#60704
1 parent e24b6d5 commit 65adb30

4 files changed

Lines changed: 82 additions & 3 deletions

File tree

goldens/public-api/core/index.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,9 @@ export interface PromiseResourceOptions<T, R> extends BaseResourceOptions<T, R>
14441444
// @public
14451445
export function provideAppInitializer(initializerFn: () => Observable<unknown> | Promise<unknown> | void): EnvironmentProviders;
14461446

1447+
// @public
1448+
export function provideBrowserGlobalErrorListeners(): EnvironmentProviders;
1449+
14471450
// @public
14481451
export function provideEnvironmentInitializer(initializerFn: () => void): EnvironmentProviders;
14491452

packages/core/src/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export {
9090
export {ApplicationModule} from './application/application_module';
9191
export {AbstractType, Type} from './interface/type';
9292
export {EventEmitter} from './event_emitter';
93-
export {ErrorHandler} from './error_handler';
93+
export {ErrorHandler, provideBrowserGlobalErrorListeners} from './error_handler';
9494
export * from './core_private_export';
9595
export * from './core_render3_private_export';
9696
export * from './core_reactivity_export';

packages/core/src/error_handler.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ENVIRONMENT_INITIALIZER, EnvironmentInjector, inject, InjectionToken} from './di';
9+
import {ENVIRONMENT_INITIALIZER} from './di/initializer_token';
10+
import {InjectionToken} from './di/injection_token';
11+
import {inject} from './di/injector_compatibility';
12+
import type {EnvironmentProviders} from './di/interface/provider';
13+
import {makeEnvironmentProviders, provideEnvironmentInitializer} from './di/provider_collection';
14+
import {EnvironmentInjector} from './di/r3_injector';
15+
import {DOCUMENT} from './document';
16+
import {DestroyRef} from './linker/destroy_ref';
1017

1118
/**
1219
* Provides a hook for centralized exception handling.
@@ -75,3 +82,46 @@ export const errorHandlerEnvironmentInitializer = {
7582
useValue: () => void inject(ErrorHandler),
7683
multi: true,
7784
};
85+
86+
const globalErrorListeners = new InjectionToken<void>(ngDevMode ? 'GlobalErrorListeners' : '', {
87+
providedIn: 'root',
88+
factory: () => {
89+
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
90+
return;
91+
}
92+
const window = inject(DOCUMENT).defaultView;
93+
if (!window) {
94+
return;
95+
}
96+
97+
const errorHandler = inject(INTERNAL_APPLICATION_ERROR_HANDLER);
98+
const rejectionListener = (e: PromiseRejectionEvent) => {
99+
errorHandler(e.reason);
100+
e.preventDefault();
101+
};
102+
const errorListener = (e: ErrorEvent) => {
103+
errorHandler(e.error);
104+
e.preventDefault();
105+
};
106+
107+
window.addEventListener('unhandledrejection', rejectionListener);
108+
window.addEventListener('error', errorListener);
109+
inject(DestroyRef).onDestroy(() => {
110+
window.removeEventListener('error', errorListener);
111+
window.removeEventListener('unhandledrejection', rejectionListener);
112+
});
113+
},
114+
});
115+
116+
/**
117+
* Provides an environment initializer which forwards unhandled errors to the ErrorHandler.
118+
*
119+
* The listeners added are for the window's 'unhandledrejection' and 'error' events.
120+
*
121+
* @publicApi
122+
*/
123+
export function provideBrowserGlobalErrorListeners(): EnvironmentProviders {
124+
return makeEnvironmentProviders([
125+
provideEnvironmentInitializer(() => void inject(globalErrorListeners)),
126+
]);
127+
}

packages/core/test/error_handler_spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ErrorHandler} from '../src/error_handler';
9+
import {TestBed} from '../testing';
10+
import {ErrorHandler, provideBrowserGlobalErrorListeners} from '../src/error_handler';
1011

1112
class MockConsole {
1213
res: any[][] = [];
@@ -38,4 +39,29 @@ describe('ErrorHandler', () => {
3839
expect(errorToString(null)).toBe('ERROR#null');
3940
expect(errorToString(undefined)).toBe('ERROR#undefined');
4041
});
42+
43+
it('installs global error handler once', async () => {
44+
if (isNode) {
45+
return;
46+
}
47+
// override global.onerror to prevent jasmine report error
48+
let originalWindowOnError = window.onerror;
49+
window.onerror = function () {};
50+
TestBed.configureTestingModule({
51+
rethrowApplicationErrors: false,
52+
providers: [provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners()],
53+
});
54+
55+
const spy = spyOn(TestBed.inject(ErrorHandler), 'handleError');
56+
await new Promise((resolve) => {
57+
setTimeout(() => {
58+
throw new Error('abc');
59+
});
60+
setTimeout(resolve, 1);
61+
});
62+
63+
expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({message: 'abc'}));
64+
expect(spy.calls.count()).toBe(1);
65+
window.onerror = originalWindowOnError;
66+
});
4167
});

0 commit comments

Comments
 (0)