Skip to content

Commit 8218d2e

Browse files
thePunderWomanatscott
authored andcommitted
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.
1 parent 8216d34 commit 8218d2e

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

packages/core/src/defer/triggering.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,12 @@ export function triggerResourceLoading(
222222
// Start downloading of defer block dependencies.
223223
tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then((results) => {
224224
let failed = false;
225+
let failedReason: Error | null = null;
225226
const directiveDefs: DirectiveDefList = [];
226227
const pipeDefs: PipeDefList = [];
227228

228-
for (const result of results) {
229+
for (let i = 0; i < results.length; i++) {
230+
const result = results[i];
229231
if (result.status === 'fulfilled') {
230232
const dependency = result.value;
231233
const directiveDef = getComponentDef(dependency) || getDirectiveDef(dependency);
@@ -239,6 +241,8 @@ export function triggerResourceLoading(
239241
}
240242
} else {
241243
failed = true;
244+
failedReason =
245+
result.reason instanceof Error ? result.reason : new Error(String(result.reason));
242246
break;
243247
}
244248
}
@@ -248,13 +252,24 @@ export function triggerResourceLoading(
248252

249253
if (tDetails.errorTmplIndex === null) {
250254
const templateLocation = ngDevMode ? getTemplateLocationDetails(lView) : '';
251-
const error = new RuntimeError(
252-
RuntimeErrorCode.DEFER_LOADING_FAILED,
253-
ngDevMode &&
255+
let errorMsg: string | false = false;
256+
257+
if (ngDevMode) {
258+
errorMsg =
254259
'Loading dependencies for `@defer` block failed, ' +
255-
`but no \`@error\` block was configured${templateLocation}. ` +
256-
'Consider using the `@error` block to render an error state.',
257-
);
260+
`but no \`@error\` block was configured${templateLocation}. ` +
261+
'Consider using the `@error` block to render an error state.';
262+
263+
const depsFnStr = tDetails.dependencyResolverFn?.toString() ?? 'unknown';
264+
const errorReason = failedReason ? failedReason.message : 'Unknown';
265+
errorMsg +=
266+
`\n\nAngular tried to invoke the following dependency function (compiler-generated):\n` +
267+
`\`\`\`\n${depsFnStr}\n\`\`\`\n` +
268+
`but it resulted in the following error:\n\n` +
269+
`${errorReason}`;
270+
}
271+
272+
const error = new RuntimeError(RuntimeErrorCode.DEFER_LOADING_FAILED, errorMsg);
258273
handleUncaughtError(lView, error);
259274
}
260275
} else {

packages/core/test/acceptance/defer_spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,74 @@ describe('@defer', () => {
11601160
expect(reportedErrors[0].message).toContain(`(used in the 'MyCmp' component template)`);
11611161
});
11621162

1163+
it('should include detailed failure info in the error message when no `@error` block is defined', async () => {
1164+
@Component({
1165+
selector: 'nested-cmp',
1166+
template: 'NestedCmp',
1167+
changeDetection: ChangeDetectionStrategy.Eager,
1168+
})
1169+
class NestedCmp {}
1170+
1171+
@Component({
1172+
selector: 'simple-app',
1173+
imports: [NestedCmp],
1174+
template: `
1175+
@defer (when isVisible) {
1176+
<nested-cmp />
1177+
} @loading {
1178+
Loading...
1179+
} @placeholder {
1180+
Placeholder
1181+
}
1182+
`,
1183+
changeDetection: ChangeDetectionStrategy.Eager,
1184+
})
1185+
class MyCmp {
1186+
isVisible = false;
1187+
}
1188+
1189+
const failedReason = new Error('Failed to load module X');
1190+
const deferDepsInterceptor = {
1191+
intercept() {
1192+
return () => [new Promise((_, reject) => setTimeout(() => reject(failedReason), 0))];
1193+
},
1194+
};
1195+
1196+
const reportedErrors: Error[] = [];
1197+
TestBed.configureTestingModule({
1198+
rethrowApplicationErrors: false,
1199+
providers: [
1200+
{
1201+
provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
1202+
useValue: deferDepsInterceptor,
1203+
},
1204+
{
1205+
provide: ErrorHandler,
1206+
useClass: class extends ErrorHandler {
1207+
override handleError(error: Error) {
1208+
reportedErrors.push(error);
1209+
}
1210+
},
1211+
},
1212+
],
1213+
});
1214+
1215+
const fixture = TestBed.createComponent(MyCmp);
1216+
fixture.detectChanges();
1217+
1218+
fixture.componentInstance.isVisible = true;
1219+
fixture.detectChanges();
1220+
1221+
await allPendingDynamicImports();
1222+
fixture.detectChanges();
1223+
1224+
expect(reportedErrors.length).toBe(1);
1225+
const errorMsg = reportedErrors[0].message;
1226+
expect(errorMsg).toContain('NG0750');
1227+
expect(errorMsg).toContain('Angular tried to invoke the following dependency function');
1228+
expect(errorMsg).toContain('Failed to load module X');
1229+
});
1230+
11631231
it('should not render `@error` block if loaded component has errors', async () => {
11641232
@Component({
11651233
selector: 'cmp-with-error',

0 commit comments

Comments
 (0)