Skip to content

Commit 048b6b1

Browse files
crisbetoalxhub
authored andcommitted
fix(core): bootstrapApplication call not rejected when error is thrown in importProvidersFrom module (#50120)
Fixes that the promise returned by `bootstrapApplication` wasn't being rejected when a module imported using `importProvidersFrom` throws an error. The problem was that the function that resolves the providers happens very early as the injector is being constructed. Fixes #49923. PR Close #50120
1 parent 21c857b commit 048b6b1

File tree

2 files changed

+108
-69
lines changed

2 files changed

+108
-69
lines changed

packages/core/src/application_ref.ts

Lines changed: 62 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -216,76 +216,80 @@ export function internalCreateApplication(config: {
216216
appProviders?: Array<Provider|EnvironmentProviders>,
217217
platformProviders?: Provider[],
218218
}): Promise<ApplicationRef> {
219-
const {rootComponent, appProviders, platformProviders} = config;
220-
221-
if ((typeof ngDevMode === 'undefined' || ngDevMode) && rootComponent !== undefined) {
222-
assertStandaloneComponentType(rootComponent);
223-
}
219+
try {
220+
const {rootComponent, appProviders, platformProviders} = config;
224221

225-
const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]);
222+
if ((typeof ngDevMode === 'undefined' || ngDevMode) && rootComponent !== undefined) {
223+
assertStandaloneComponentType(rootComponent);
224+
}
226225

227-
// Create root application injector based on a set of providers configured at the platform
228-
// bootstrap level as well as providers passed to the bootstrap call by a user.
229-
const allAppProviders = [
230-
provideZoneChangeDetection(),
231-
...(appProviders || []),
232-
];
233-
const adapter = new EnvironmentNgModuleRefAdapter({
234-
providers: allAppProviders,
235-
parent: platformInjector as EnvironmentInjector,
236-
debugName: (typeof ngDevMode === 'undefined' || ngDevMode) ? 'Environment Injector' : '',
237-
// We skip environment initializers because we need to run them inside the NgZone, which happens
238-
// after we get the NgZone instance from the Injector.
239-
runEnvironmentInitializers: false,
240-
});
241-
const envInjector = adapter.injector;
242-
const ngZone = envInjector.get(NgZone);
226+
const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]);
227+
228+
// Create root application injector based on a set of providers configured at the platform
229+
// bootstrap level as well as providers passed to the bootstrap call by a user.
230+
const allAppProviders = [
231+
provideZoneChangeDetection(),
232+
...(appProviders || []),
233+
];
234+
const adapter = new EnvironmentNgModuleRefAdapter({
235+
providers: allAppProviders,
236+
parent: platformInjector as EnvironmentInjector,
237+
debugName: (typeof ngDevMode === 'undefined' || ngDevMode) ? 'Environment Injector' : '',
238+
// We skip environment initializers because we need to run them inside the NgZone, which
239+
// happens after we get the NgZone instance from the Injector.
240+
runEnvironmentInitializers: false,
241+
});
242+
const envInjector = adapter.injector;
243+
const ngZone = envInjector.get(NgZone);
243244

244-
return ngZone.run(() => {
245-
envInjector.resolveInjectorInitializers();
246-
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
247-
if ((typeof ngDevMode === 'undefined' || ngDevMode) && !exceptionHandler) {
248-
throw new RuntimeError(
249-
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
250-
'No `ErrorHandler` found in the Dependency Injection tree.');
251-
}
245+
return ngZone.run(() => {
246+
envInjector.resolveInjectorInitializers();
247+
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
248+
if ((typeof ngDevMode === 'undefined' || ngDevMode) && !exceptionHandler) {
249+
throw new RuntimeError(
250+
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
251+
'No `ErrorHandler` found in the Dependency Injection tree.');
252+
}
252253

253-
let onErrorSubscription: Subscription;
254-
ngZone.runOutsideAngular(() => {
255-
onErrorSubscription = ngZone.onError.subscribe({
256-
next: (error: any) => {
257-
exceptionHandler!.handleError(error);
258-
}
254+
let onErrorSubscription: Subscription;
255+
ngZone.runOutsideAngular(() => {
256+
onErrorSubscription = ngZone.onError.subscribe({
257+
next: (error: any) => {
258+
exceptionHandler!.handleError(error);
259+
}
260+
});
259261
});
260-
});
261262

262-
// If the whole platform is destroyed, invoke the `destroy` method
263-
// for all bootstrapped applications as well.
264-
const destroyListener = () => envInjector.destroy();
265-
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS);
266-
onPlatformDestroyListeners.add(destroyListener);
263+
// If the whole platform is destroyed, invoke the `destroy` method
264+
// for all bootstrapped applications as well.
265+
const destroyListener = () => envInjector.destroy();
266+
const onPlatformDestroyListeners = platformInjector.get(PLATFORM_DESTROY_LISTENERS);
267+
onPlatformDestroyListeners.add(destroyListener);
267268

268-
envInjector.onDestroy(() => {
269-
onErrorSubscription.unsubscribe();
270-
onPlatformDestroyListeners.delete(destroyListener);
271-
});
269+
envInjector.onDestroy(() => {
270+
onErrorSubscription.unsubscribe();
271+
onPlatformDestroyListeners.delete(destroyListener);
272+
});
272273

273-
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
274-
const initStatus = envInjector.get(ApplicationInitStatus);
275-
initStatus.runInitializers();
274+
return _callAndReportToErrorHandler(exceptionHandler!, ngZone, () => {
275+
const initStatus = envInjector.get(ApplicationInitStatus);
276+
initStatus.runInitializers();
276277

277-
return initStatus.donePromise.then(() => {
278-
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
279-
setLocaleId(localeId || DEFAULT_LOCALE_ID);
278+
return initStatus.donePromise.then(() => {
279+
const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID);
280+
setLocaleId(localeId || DEFAULT_LOCALE_ID);
280281

281-
const appRef = envInjector.get(ApplicationRef);
282-
if (rootComponent !== undefined) {
283-
appRef.bootstrap(rootComponent);
284-
}
285-
return appRef;
282+
const appRef = envInjector.get(ApplicationRef);
283+
if (rootComponent !== undefined) {
284+
appRef.bootstrap(rootComponent);
285+
}
286+
return appRef;
287+
});
286288
});
287289
});
288-
});
290+
} catch (e) {
291+
return Promise.reject(e);
292+
}
289293
}
290294

291295
/**

packages/platform-browser/test/browser/bootstrap_spec.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {animate, style, transition, trigger} from '@angular/animations';
1010
import {DOCUMENT, isPlatformBrowser, ɵgetDOM as getDOM} from '@angular/common';
11-
import {ANIMATION_MODULE_TYPE, APP_INITIALIZER, Compiler, Component, createPlatformFactory, CUSTOM_ELEMENTS_SCHEMA, Directive, ErrorHandler, Inject, inject as _inject, InjectionToken, Injector, LOCALE_ID, NgModule, NgModuleRef, NgZone, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Provider, Sanitizer, StaticProvider, Testability, TestabilityRegistry, TransferState, Type, VERSION} from '@angular/core';
11+
import {ANIMATION_MODULE_TYPE, APP_INITIALIZER, Compiler, Component, createPlatformFactory, CUSTOM_ELEMENTS_SCHEMA, Directive, ErrorHandler, importProvidersFrom, Inject, inject as _inject, InjectionToken, Injector, LOCALE_ID, NgModule, NgModuleRef, NgZone, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Provider, Sanitizer, StaticProvider, Testability, TestabilityRegistry, TransferState, Type, VERSION} from '@angular/core';
1212
import {ApplicationRef, destroyPlatform, provideZoneChangeDetection} from '@angular/core/src/application_ref';
1313
import {Console} from '@angular/core/src/console';
1414
import {ComponentRef} from '@angular/core/src/linker/component_factory';
@@ -288,14 +288,22 @@ function bootstrap(
288288
expect(el2.innerText).toBe('Hello from Updated NonStandaloneComp!');
289289
});
290290

291-
it('should throw when trying to bootstrap a non-standalone component', () => {
291+
it('should throw when trying to bootstrap a non-standalone component', async () => {
292292
const msg = 'NG0907: The NonStandaloneComp component is not marked as standalone, ' +
293293
'but Angular expects to have a standalone component here. Please make sure the ' +
294294
'NonStandaloneComp component has the `standalone: true` flag in the decorator.';
295-
expect(() => bootstrapApplication(NonStandaloneComp)).toThrowError(msg);
295+
let bootstrapError: string|null = null;
296+
297+
try {
298+
await bootstrapApplication(NonStandaloneComp);
299+
} catch (e) {
300+
bootstrapError = (e as Error).message;
301+
}
302+
303+
expect(bootstrapError).toBe(msg);
296304
});
297305

298-
it('should throw when trying to bootstrap a standalone directive', () => {
306+
it('should throw when trying to bootstrap a standalone directive', async () => {
299307
@Directive({
300308
standalone: true,
301309
selector: '[dir]',
@@ -306,15 +314,31 @@ function bootstrap(
306314
const msg = //
307315
'NG0906: The StandaloneDirective is not an Angular component, ' +
308316
'make sure it has the `@Component` decorator.';
309-
expect(() => bootstrapApplication(StandaloneDirective)).toThrowError(msg);
317+
let bootstrapError: string|null = null;
318+
319+
try {
320+
await bootstrapApplication(StandaloneDirective);
321+
} catch (e) {
322+
bootstrapError = (e as Error).message;
323+
}
324+
325+
expect(bootstrapError).toBe(msg);
310326
});
311327

312-
it('should throw when trying to bootstrap a non-annotated class', () => {
328+
it('should throw when trying to bootstrap a non-annotated class', async () => {
313329
class NonAnnotatedClass {}
314330
const msg = //
315331
'NG0906: The NonAnnotatedClass is not an Angular component, ' +
316332
'make sure it has the `@Component` decorator.';
317-
expect(() => bootstrapApplication(NonAnnotatedClass)).toThrowError(msg);
333+
let bootstrapError: string|null = null;
334+
335+
try {
336+
await bootstrapApplication(NonAnnotatedClass);
337+
} catch (e) {
338+
bootstrapError = (e as Error).message;
339+
}
340+
341+
expect(bootstrapError).toBe(msg);
318342
});
319343

320344
it('should have the TransferState token available', async () => {
@@ -334,14 +358,25 @@ function bootstrap(
334358
expect(state).toBeInstanceOf(TransferState);
335359
});
336360

361+
it('should reject the bootstrapApplication promise if an imported module throws', (done) => {
362+
@NgModule()
363+
class ErrorModule {
364+
constructor() {
365+
throw new Error('This error should be in the promise rejection');
366+
}
367+
}
368+
369+
bootstrapApplication(SimpleComp, {
370+
providers: [importProvidersFrom(ErrorModule)]
371+
}).then(() => done.fail('Expected bootstrap promised to be rejected'), () => done());
372+
});
373+
337374
describe('with animations', () => {
338375
@Component({
339376
standalone: true,
340377
selector: 'hello-app',
341-
template: `
342-
<div
343-
@myAnimation
344-
(@myAnimation.start)="onStart($event)">Hello from AnimationCmp!</div>`,
378+
template:
379+
'<div @myAnimation (@myAnimation.start)="onStart($event)">Hello from AnimationCmp!</div>',
345380
animations: [trigger(
346381
'myAnimation', [transition('void => *', [style({opacity: 1}), animate(5)])])],
347382
})

0 commit comments

Comments
 (0)