From e583f4c80811202ffbd321b98cc957fd586df1bd Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Tue, 31 Mar 2026 11:50:31 -0700 Subject: [PATCH] refactor(core): Add more detail to NG0750 error message This adds a bit more context to the NG0750 error message to provide details about which module failed to load when executing the dependencyResolverFn. This can help with debugging a failed lazy load in a defer block. (cherry picked from commit 8218d2e34a48601ca34aababf104fe4f727843e5) --- packages/core/src/defer/triggering.ts | 29 ++++++--- packages/core/test/acceptance/defer_spec.ts | 68 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/packages/core/src/defer/triggering.ts b/packages/core/src/defer/triggering.ts index 8a11d2d7d89..2f6869d7f5b 100644 --- a/packages/core/src/defer/triggering.ts +++ b/packages/core/src/defer/triggering.ts @@ -222,10 +222,12 @@ export function triggerResourceLoading( // Start downloading of defer block dependencies. tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then((results) => { let failed = false; + let failedReason: Error | null = null; const directiveDefs: DirectiveDefList = []; const pipeDefs: PipeDefList = []; - for (const result of results) { + for (let i = 0; i < results.length; i++) { + const result = results[i]; if (result.status === 'fulfilled') { const dependency = result.value; const directiveDef = getComponentDef(dependency) || getDirectiveDef(dependency); @@ -239,6 +241,8 @@ export function triggerResourceLoading( } } else { failed = true; + failedReason = + result.reason instanceof Error ? result.reason : new Error(String(result.reason)); break; } } @@ -248,13 +252,24 @@ export function triggerResourceLoading( if (tDetails.errorTmplIndex === null) { const templateLocation = ngDevMode ? getTemplateLocationDetails(lView) : ''; - const error = new RuntimeError( - RuntimeErrorCode.DEFER_LOADING_FAILED, - ngDevMode && + let errorMsg: string | false = false; + + if (ngDevMode) { + errorMsg = 'Loading dependencies for `@defer` block failed, ' + - `but no \`@error\` block was configured${templateLocation}. ` + - 'Consider using the `@error` block to render an error state.', - ); + `but no \`@error\` block was configured${templateLocation}. ` + + 'Consider using the `@error` block to render an error state.'; + + const depsFnStr = tDetails.dependencyResolverFn?.toString() ?? 'unknown'; + const errorReason = failedReason ? failedReason.message : 'Unknown'; + errorMsg += + `\n\nAngular tried to invoke the following dependency function (compiler-generated):\n` + + `\`\`\`\n${depsFnStr}\n\`\`\`\n` + + `but it resulted in the following error:\n\n` + + `${errorReason}`; + } + + const error = new RuntimeError(RuntimeErrorCode.DEFER_LOADING_FAILED, errorMsg); handleUncaughtError(lView, error); } } else { diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts index 59dfb90848b..a65c6ad748b 100644 --- a/packages/core/test/acceptance/defer_spec.ts +++ b/packages/core/test/acceptance/defer_spec.ts @@ -1034,6 +1034,74 @@ describe('@defer', () => { expect(reportedErrors[0].message).toContain(`(used in the 'MyCmp' component template)`); }); + it('should include detailed failure info in the error message when no `@error` block is defined', async () => { + @Component({ + selector: 'nested-cmp', + template: 'NestedCmp', + changeDetection: ChangeDetectionStrategy.Eager, + }) + class NestedCmp {} + + @Component({ + selector: 'simple-app', + imports: [NestedCmp], + template: ` + @defer (when isVisible) { + + } @loading { + Loading... + } @placeholder { + Placeholder + } + `, + changeDetection: ChangeDetectionStrategy.Eager, + }) + class MyCmp { + isVisible = false; + } + + const failedReason = new Error('Failed to load module X'); + const deferDepsInterceptor = { + intercept() { + return () => [new Promise((_, reject) => setTimeout(() => reject(failedReason), 0))]; + }, + }; + + const reportedErrors: Error[] = []; + TestBed.configureTestingModule({ + rethrowApplicationErrors: false, + providers: [ + { + provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, + useValue: deferDepsInterceptor, + }, + { + provide: ErrorHandler, + useClass: class extends ErrorHandler { + override handleError(error: Error) { + reportedErrors.push(error); + } + }, + }, + ], + }); + + const fixture = TestBed.createComponent(MyCmp); + fixture.detectChanges(); + + fixture.componentInstance.isVisible = true; + fixture.detectChanges(); + + await allPendingDynamicImports(); + fixture.detectChanges(); + + expect(reportedErrors.length).toBe(1); + const errorMsg = reportedErrors[0].message; + expect(errorMsg).toContain('NG0750'); + expect(errorMsg).toContain('Angular tried to invoke the following dependency function'); + expect(errorMsg).toContain('Failed to load module X'); + }); + it('should not render `@error` block if loaded component has errors', async () => { @Component({ selector: 'cmp-with-error',