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 8218d2e34a)
This commit is contained in:
Jessica Janiuk 2026-03-31 11:50:31 -07:00 committed by Andrew Scott
parent 05bcc601ac
commit e583f4c808
2 changed files with 90 additions and 7 deletions

View file

@ -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 {

View file

@ -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) {
<nested-cmp />
} @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',