refactor(core): report @defer errors using ErrorHandler (#52320)

This commit updates the code to report errors via `ErrorHandler` instance.
For dependency loading problems, errors are reported only when `@error` block is not provided.

PR Close #52320
This commit is contained in:
Andrew Kushnir
2023-10-22 21:26:32 -07:00
committed by Dylan Hunn
parent a87586538a
commit ec1e652f8a
5 changed files with 181 additions and 2 deletions
+2
View File
@@ -29,6 +29,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
CYCLIC_DI_DEPENDENCY = -200,
// (undocumented)
DEFER_LOADING_FAILED = 750,
// (undocumented)
DUPLICATE_DIRECTITVE = 309,
// (undocumented)
EXPORT_NOT_FOUND = -301,
+19 -1
View File
@@ -7,12 +7,15 @@
*/
import {InjectionToken, Injector} from '../di';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {findMatchingDehydratedView} from '../hydration/views';
import {populateDehydratedViewsInLContainer} from '../linker/view_container_ref';
import {assertLContainer, assertTNodeForLView} from '../render3/assert';
import {bindingUpdated} from '../render3/bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/definition';
import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
import {markViewDirty} from '../render3/instructions/mark_view_dirty';
import {handleError} from '../render3/instructions/shared';
import {ɵɵtemplate} from '../render3/instructions/template';
import {LContainer} from '../render3/interfaces/container';
import {DirectiveDefList, PipeDefList} from '../render3/interfaces/definition';
@@ -465,7 +468,11 @@ export function renderDeferBlockState(
const applyStateFn =
needsScheduling ? applyDeferBlockStateWithSchedulingImpl! : applyDeferBlockState;
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
try {
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
} catch (error: unknown) {
handleError(hostLView, error);
}
}
}
@@ -667,6 +674,17 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
if (failed) {
tDetails.loadingState = DeferDependenciesLoadingState.FAILED;
if (tDetails.errorTmplIndex === null) {
const templateLocation = getTemplateLocationDetails(lView);
const error = new RuntimeError(
RuntimeErrorCode.DEFER_LOADING_FAILED,
ngDevMode &&
'Loading dependencies for `@defer` block failed, ' +
`but no \`@error\` block was configured${templateLocation}. ` +
'Consider using the `@error` block to render an error state.');
handleError(lView, error);
}
} else {
tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE;
+3
View File
@@ -93,6 +93,9 @@ export const enum RuntimeErrorCode {
INVALID_I18N_STRUCTURE = 700,
MISSING_LOCALE_DATA = 701,
// Defer errors (750-799 range)
DEFER_LOADING_FAILED = 750,
// standalone errors
IMPORT_PROVIDERS_FROM_STANDALONE = 800,
+151 -1
View File
@@ -7,7 +7,7 @@
*/
import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, inject, Input, NgZone, Pipe, PipeTransform, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, ErrorHandler, inject, Input, NgZone, Pipe, PipeTransform, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
import {getComponentDef} from '@angular/core/src/render3/definition';
import {ComponentFixture, DeferBlockBehavior, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
@@ -863,6 +863,156 @@ describe('@defer', () => {
expect(fixture.componentInstance.cmps.length).toBe(1);
expect(fixture.componentInstance.cmps.get(0)?.block).toBe('error');
});
it('should report an error to the ErrorHandler if no `@error` block is defined', async () => {
@Component({
selector: 'nested-cmp',
standalone: true,
template: 'NestedCmp',
})
class NestedCmp {
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [NestedCmp],
template: `
@defer (when isVisible) {
<nested-cmp />
} @loading {
Loading...
} @placeholder {
Placeholder
}
`
})
class MyCmp {
isVisible = false;
}
const deferDepsInterceptor = {
intercept() {
return () => [failedDynamicImport()];
}
};
const reportedErrors: Error[] = [];
TestBed.configureTestingModule({
providers: [
{
provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
useValue: deferDepsInterceptor,
},
{
provide: ErrorHandler, useClass: class extends ErrorHandler{
override handleError(error: Error) {
reportedErrors.push(error);
}
},
},
],
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Loading');
// Wait for dependencies to load.
await allPendingDynamicImports();
fixture.detectChanges();
// Verify that there was an error reported to the `ErrorHandler`.
expect(reportedErrors.length).toBe(1);
expect(reportedErrors[0].message).toContain('NG0750');
expect(reportedErrors[0].message).toContain(`(used in the 'MyCmp' component template)`);
});
it('should not render `@error` block if loaded component has errors', async () => {
@Component({
selector: 'cmp-with-error',
standalone: true,
template: 'CmpWithError',
})
class CmpWithError {
constructor() {
throw new Error('CmpWithError produced an error');
}
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [CmpWithError],
template: `
@defer (when isVisible) {
<cmp-with-error />
} @loading {
Loading...
} @error {
Error
} @placeholder {
Placeholder
}
`
})
class MyCmp {
isVisible = false;
}
const deferDepsInterceptor = {
intercept() {
return () => [dynamicImportOf(CmpWithError)];
}
};
const reportedErrors: Error[] = [];
TestBed.configureTestingModule({
providers: [
{
provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
useValue: deferDepsInterceptor,
},
{
provide: ErrorHandler, useClass: class extends ErrorHandler{
override handleError(error: Error) {
reportedErrors.push(error);
}
},
},
],
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Loading');
// Wait for dependencies to load.
await allPendingDynamicImports();
fixture.detectChanges();
// Expect an error to be reported to the `ErrorHandler`.
expect(reportedErrors.length).toBe(1);
expect(reportedErrors[0].message).toBe('CmpWithError produced an error');
// Expect that the `@loading` UI is removed, but the `@error` is *not* rendered,
// because it was a component initialization error, not resource loading issue.
expect(fixture.nativeElement.textContent).toBe('');
});
});
describe('queries', () => {
@@ -962,6 +962,9 @@
{
"name": "getTView"
},
{
"name": "handleError"
},
{
"name": "hasInSkipHydrationBlockFlag"
},
@@ -2363,6 +2366,9 @@
{
"name": "switchMap"
},
{
"name": "throwError"
},
{
"name": "throwProviderNotFoundError"
},