Reactive Error Handling in Angular

The example provides a stream of values designed to demonstrate successful operations and robust error handling, specifically illustrating how to selectively capture and display error states within the user interface.

Published: 6/6/2024

This article builds upon my async pipe article, practically demonstrating a clean and efficient method to handle errors in Angular.

The stream is just to illustrate error handling, as selected form a stream of successes.

Stream: loading -> content -> content -> error -> (terminated).

import {Component, inject, Injectable} from '@angular/core';
import {AsyncPipe, NgIf} from '@angular/common';
import {catchError, concatMap, delay, ignoreElements, of, tap} from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe, NgIf],
  template: `
    <ng-container *ngIf="{user: user$ | async, userError: userError$ | async} as viewModel">
      <div *ngIf="!viewModel.userError && viewModel.user as user; else loading">
        <h2>Content template</h2>
        <p>{{ user }}</p>
      </div>

      <div *ngIf="viewModel.userError as error">
        <h2>Error template</h2>
        <p>{{ error }}</p>
      </div>

      <ng-template #loading>
        <div *ngIf="!viewModel.userError">
          <h2>Loading template</h2>
          <p>Loading...</p>
        </div>
      </ng-template>
    </ng-container>
  `,
})
export class AppComponent {
  appService = inject(AppService);

  user$ = this.appService.getUserStream();
  userError$ = this.user$.pipe(
    ignoreElements(),
    catchError((err) => of(err))
  );
}

@Injectable({
  providedIn: 'root'
})
export class AppService {
  getUserStream() {
    return of('Alice', 'Bob', 'Err', 'Charlie').pipe(
      concatMap((user) => of(user).pipe(delay(1000))),
      tap((user) => {
        if (user === 'Err') {
          throw new Error('Could not fetch user');
        }
      })
    );
  }
}
  • The UI switches between “Content,” “Error,” and “Loading” states, ensuring only one is visible at a time.
  • The outer *ngIf on ng-container is just used for setup, the actual conditional is irrelevant.
  • It establishes a viewModel that centralises unwrapped observable values, making template logic cleaner and providing access to all properties internally.
  • AsyncPipe automatically handles subscribing to and unsubscribing from observables within the template, preventing memory leaks and simplifying data binding.
  • of() is used to mock a server response, converting arguments into an observable stream sequence.
  • concatMap processes mock user values one by one, introducing a delay to simulate asynchronous operations and ensure order.
  • tap is used for side effects and conditional error injection, inspecting values and simulating an error condition by throwing an exception when ‘Err’ is encountered.
  • When the user ‘Err’ is read, the stream throws an error and is subsequently destroyed.
  • The error thrown by tap causes the primary stream to error out and effectively terminate, ceasing further emissions.
  • userError$ utilises ignoreElements() to ensure it only emits when an error occurs in the source user$ stream, acting as a pure error indicator.
  • catchError intercepts the error from the main stream, transforming it into a new observable that emits the error message, allowing the UI to display the error gracefully without the entire error stream terminating.