1

I'm using Angular 20, and I need to create a component that works like a React Error Boundary.

The idea is:

  • If there are no errors inside the child components, it should render the normal content.

  • If an error occurs in any of the child components, it should display a fallback UI instead.

Here’s an example of what I mean in template form:

<error-boundary>
  <some-inner-component />
</error-boundary>

If throws an error, the ErrorBoundaryComponent should catch it and render a fallback template like this:

<div class="error-fallback">
  <h2>Something went wrong</h2>
  <button (click)="reset()">Try Again</button>
</div>

For the beginning I expect, that SomeInnerComponent will have smth like this in constructor

constructor() {
  throw new Error('Error...')
}

and it should be handled by <error-boundary>

1 Answer 1

1

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,
    },
  ],
});

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

1 Comment

Cool, thanks a lot

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.