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/2024This article builds upon my async pipe article, practically demonstrating a clean and efficient method to handle errors in Angular.
The code#
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');
}
})
);
}
}
Template Logic and Data Presentation#
- The UI switches between “Content,” “Error,” and “Loading” states, ensuring only one is visible at a time.
- The outer
*ngIfonng-containeris just used for setup, the actual conditional is irrelevant. - It establishes a
viewModelthat centralises unwrapped observable values, making template logic cleaner and providing access to all properties internally. AsyncPipeautomatically handles subscribing to and unsubscribing from observables within the template, preventing memory leaks and simplifying data binding.
Data Sourcing and Stream Behavior#
of()is used to mock a server response, converting arguments into an observable stream sequence.concatMapprocesses mock user values one by one, introducing a delay to simulate asynchronous operations and ensure order.tapis used for side effects and conditional error injection, inspecting values and simulating an error condition by throwing an exception when ‘Err’ is encountered.
Error Handling Implementation#
- When the user ‘Err’ is read, the stream throws an error and is subsequently destroyed.
- The error thrown by
tapcauses the primary stream to error out and effectively terminate, ceasing further emissions. userError$utilisesignoreElements()to ensure it only emits when an error occurs in the sourceuser$stream, acting as a pure error indicator.catchErrorintercepts 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.