Skip to content

Commit 1a66a21

Browse files
committed
fix(core): avoid injecting ErrorHandler from a destroyed injector
This commit prevents lazy injection of the internal `ErrorHandler` from a destroyed injector, as it would result in another "destroyed injector" error.
1 parent b839d08 commit 1a66a21

File tree

3 files changed

+70
-5
lines changed

3 files changed

+70
-5
lines changed

packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,18 @@ export function internalProvideZoneChangeDetection({
139139
const injector = inject(EnvironmentInjector);
140140
let userErrorHandler: ErrorHandler;
141141
return (e: unknown) => {
142-
userErrorHandler ??= injector.get(ErrorHandler);
143-
zone.runOutsideAngular(() => userErrorHandler.handleError(e));
142+
zone.runOutsideAngular(() => {
143+
if (injector.destroyed) {
144+
setTimeout(() => {
145+
throw e;
146+
});
147+
} else if (userErrorHandler) {
148+
userErrorHandler.handleError(e);
149+
} else {
150+
userErrorHandler ??= injector.get(ErrorHandler);
151+
userErrorHandler.handleError(e);
152+
}
153+
});
144154
};
145155
},
146156
},

packages/core/src/error_handler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,16 @@ export const INTERNAL_APPLICATION_ERROR_HANDLER = new InjectionToken<(e: any) =>
7070
const injector = inject(EnvironmentInjector);
7171
let userErrorHandler: ErrorHandler;
7272
return (e: unknown) => {
73-
userErrorHandler ??= injector.get(ErrorHandler);
74-
userErrorHandler.handleError(e);
73+
if (injector.destroyed) {
74+
setTimeout(() => {
75+
throw e;
76+
});
77+
} else if (userErrorHandler) {
78+
userErrorHandler.handleError(e);
79+
} else {
80+
userErrorHandler ??= injector.get(ErrorHandler);
81+
userErrorHandler.handleError(e);
82+
}
7583
};
7684
},
7785
},

packages/core/test/error_handler_spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import {TestBed} from '../testing';
1010
import {ErrorHandler, provideBrowserGlobalErrorListeners} from '../src/error_handler';
11-
import {isNode} from '@angular/private/testing';
11+
import {isNode, withBody} from '@angular/private/testing';
12+
import {ApplicationRef, Component, destroyPlatform, inject} from '../src/core';
13+
import {bootstrapApplication} from '@angular/platform-browser';
1214

1315
class MockConsole {
1416
res: any[][] = [];
@@ -65,4 +67,49 @@ describe('ErrorHandler', () => {
6567
expect(spy.calls.count()).toBe(1);
6668
window.onerror = originalWindowOnError;
6769
});
70+
71+
it(
72+
'should not try to inject the `ErrorHandler` lazily once app is destroyed',
73+
withBody('<app></app>', async () => {
74+
destroyPlatform();
75+
76+
let dispatched = false;
77+
// Prevents Jasmine from reporting an error.
78+
const originalWindowOnError = window.onerror;
79+
window.onerror = () => {};
80+
81+
@Component({
82+
selector: 'app',
83+
template: '',
84+
})
85+
class App {
86+
constructor() {
87+
inject(ApplicationRef).onDestroy(() => {
88+
// Note: The unit test environment differs from the real browser environment.
89+
// This is a simple test that ensures that if an error event is dispatched
90+
// during destruction, it does not attempt to inject the `ErrorHandler`.
91+
// Before the `if (injector.destroyed)` checks were added, this would
92+
// throw a "destroyed injector" error.
93+
dispatched = window.dispatchEvent(new Event('error'));
94+
});
95+
}
96+
}
97+
98+
const appRef = await bootstrapApplication(App, {
99+
providers: [provideBrowserGlobalErrorListeners()],
100+
});
101+
appRef.destroy();
102+
103+
// We assert that `dispatched` is truthy because Angular's error handler
104+
// calls `preventDefault()` on the event object, which would cause `dispatchEvent`
105+
// to return false. This assertion ensures that Angular's error handler was not invoked.
106+
expect(dispatched).toEqual(true);
107+
108+
// Wait until the error is re-thrown, so we can reset the original error handler.
109+
await new Promise((resolve) => setTimeout(resolve, 1));
110+
111+
window.onerror = originalWindowOnError;
112+
destroyPlatform();
113+
}),
114+
);
68115
});

0 commit comments

Comments
 (0)