I think it is not worth the tedious effort, but a boundary-error component will look like below.
First we extend the JavaScript Error Class and pass in our custom properties.
class BoundaryError extends Error {
boundaryHash: string | null;
constructor(message: string, boundaryHash: string | null) {
super(message);
this.name = 'boundaryError';
this.boundaryHash = boundaryHash; // Custom property
}
}
The basic concept is, boundaryHash is a unique value which can be used to identify boundary errors.
Now we can create the boundary-error component, where we leverage ng-content multiple projection, to pass the main content and the fallback content.
@Component({
selector: 'error-boundary',
template: `
@let hasError = boundaryHash != errorBoundaryService.boundaryHash;
<div [hidden]="!hasError">
<ng-content select="[content]"/>
</div>
<div [hidden]="hasError">
<ng-content select="[fallback]"/>
</div>
`,
providers: [{ useValue: Math.random(), provide: BOUNDARY_HASH }],
})
export class ErrorBoundary {
boundaryHash = inject(BOUNDARY_HASH);
errorBoundaryService = inject(ErrorBoundaryService);
}
Note: In the above code, we use Dependency Injection to create a unique number using random for each boundary component.
We also create a service that will store this hash value.
@Injectable({
providedIn: 'root',
})
export class ErrorBoundaryService {
boundaryHash: string | null = null;
}
Now we can setup a global error handler. This will catch the BoundaryError and store the hash value.
class MyErrorHandler implements ErrorHandler {
ebs = inject(ErrorBoundaryService);
handleError(error: any) {
console.log(error);
// do something with the exception
if (error instanceof BoundaryError) {
this.ebs.boundaryHash = error.boundaryHash;
}
}
}
bootstrapApplication(App, {
providers: [
{
provide: ErrorHandler,
useClass: MyErrorHandler,
},
],
});
We just need to call BoundaryError and pass in the boundaryHash which we fetch using DI.
@Component({
selector: 'large-component',
template: `large component`,
})
export class LargeComponent {
boundaryHash = inject(BOUNDARY_HASH, { optional: true });
constructor() {}
ngOnInit() {
throw new BoundaryError('Error...', this.boundaryHash);
}
}
Note: Calling an error on the constructor will always cause the component to fail, so call the errors on the lifecycle hooks instead.
Finally we put it all together and pass in the content and fallback HTML.
@Component({
selector: 'app-root',
imports: [LargeComponent, ErrorBoundary],
template: `
<error-boundary>
<large-component content/>
<div fallback>
<div class="error-fallback">
<h2>Something went wrong</h2>
<button>Try Again</button>
</div>
</div>
</error-boundary>
`,
})
export class App {
name = 'Angular';
}
Full Code:
import 'zone.js';
import {
Component,
ErrorHandler,
inject,
Injectable,
InjectionToken,
runInInjectionContext,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
export const BOUNDARY_HASH = new InjectionToken<string>('boundaryHash');
class BoundaryError extends Error {
boundaryHash: string | null;
constructor(message: string, boundaryHash: string | null) {
super(message);
this.name = 'boundaryError';
this.boundaryHash = boundaryHash; // Custom property
}
}
@Injectable({
providedIn: 'root',
})
export class ErrorBoundaryService {
boundaryHash: string | null = null;
}
@Component({
selector: 'error-boundary',
template: `
@let hasError = boundaryHash != errorBoundaryService.boundaryHash;
<div [hidden]="!hasError">
<ng-content select="[content]"/>
</div>
<div [hidden]="hasError">
<ng-content select="[fallback]"/>
</div>
`,
providers: [{ useValue: Math.random(), provide: BOUNDARY_HASH }],
})
export class ErrorBoundary {
boundaryHash = inject(BOUNDARY_HASH);
errorBoundaryService = inject(ErrorBoundaryService);
}
@Component({
selector: 'large-component',
template: `large component`,
})
export class LargeComponent {
boundaryHash = inject(BOUNDARY_HASH, { optional: true });
constructor() {}
ngOnInit() {
throw new BoundaryError('Error...', this.boundaryHash);
}
}
@Component({
selector: 'app-root',
imports: [LargeComponent, ErrorBoundary],
template: `
<error-boundary>
<large-component content/>
<div fallback>
<div class="error-fallback">
<h2>Something went wrong</h2>
<button>Try Again</button>
</div>
</div>
</error-boundary>
`,
})
export class App {
name = 'Angular';
}
class MyErrorHandler implements ErrorHandler {
ebs = inject(ErrorBoundaryService);
handleError(error: any) {
console.log(error);
// do something with the exception
if (error instanceof BoundaryError) {
this.ebs.boundaryHash = error.boundaryHash;
}
}
}
bootstrapApplication(App, {
providers: [
{
provide: ErrorHandler,
useClass: MyErrorHandler,
},
],
});