Tutorial hero image
Lesson icon

How to Handle Errors Reactively when Using the Async Pipe

4 min read

Originally published May 06, 2022

One common thing I hear about using the async pipe to automatically subscribe to observable streams in the template:

<app-user-card [user]="user$ | async"></app-user-card>

...is something to the effect of:

"Sure, it looks nice in a tutorial, but when you have to handle errors it gets ugly"

To demonstrate a way you can still handle errors nicely with the async pipe - or at least I think so - I published this video. The general idea is that you just have two streams: a stream for your data, and a separate stream for errors.

This ends up looking like this:

export class HomePage {
  user$ = this.userService.getUser();
  userError$ = this.user$.pipe(
    ignoreElements(),
    catchError((err) => of(err))
  );

  constructor(public userService: UserService) {}
}
  <ng-container
    *ngIf="{user: user$ | async, userError: userError$ | async} as vm"
  >
    <ion-card *ngIf="!vm.userError && vm.user as user; else loading">
      <ion-card-content>
        <p>{{ user }}</p>
      </ion-card-content>
    </ion-card>
    <ion-note *ngIf="vm.userError as error"> {{ error }} </ion-note>

    <ng-template #loading>
      <ion-card *ngIf="!vm.userError">
        <ion-card-content>
          <ion-skeleton-text animated></ion-skeleton-text>
        </ion-card-content>
      </ion-card>
    </ng-template>
  </ng-container>

This method is fine, but there is still some level of awkwardness to it. Probably the most notable being that you have to be careful not to actually trigger your observable twice, e.g:

  getFromAPI() {
    // This creates a hot observable and we cache it on a class member
    // so that multiple subscribers don't trigger multiple requests
    if (!this.apiExample) {
      this.apiExample = this.http
        .get('https://jsonplaceholder.typicode.com/todos/1')
        .pipe(
          tap(() => console.log('setting up stream!')),
          shareReplay(1)
        );
    }

    return this.apiExample;
  }

If you were making an HTTP request for example, you would need to make sure to cache the observable and share the response (e.g. with shareReplay) so that you don't actually fire off two separate requests - one for your main data stream, and one for the additional error stream:

  <ng-container
    *ngIf="{user: user$ | async, userError: userError$ | async} as vm"
  >

For more information on caching observables, you can check out: Caching and Sharing Firestore Data with RxJS and ShareReplay

Another thing that can be improved is that we can simplify the template quite significantly if we abstract some functionality out into a dumb component.

That is what we are going to do in this tutorial - we will be taking a look at a more optimised version of the same basic principle from the previous video: having a separate stream for errors that are displayed using the async pipe. This requires some additional dependencies and abstractions, but the end result is very clean.

I will be using @ngrx/component-store to handle this, but there are plenty of other ways you can handle this scenario that I won't be covering - I just happen to really like Component Store and use it everywhere. In particular, the ngrxLet directive is a great way to handle stream errors if you don't want to incorporate a state management solution like Component Store.

The key point to making the async pipe work well with errors is to make sure you are using a consistent declarative/reactive approach - utilising the streams to handle errors - rather than mixing a reactive approach with imperative style programming (e.g. using the async pipe, but manually subscribing to streams and handling errors outside of the streams).

You can view a video version of this tutorial below:

Outline

Source code

The Plan

This tutorial will heavily utilise concepts I have covered in previous tutorials: one that covers the general idea of using an error stream with the async pipe, which is what I used as a base for this tutorial - and also a separate tutorial on having dumb components supply their own loading template:

I will be using concepts from both of these tutorials, but I won't be covering those concepts in detail in this tutorials, so make sure to go back and complete those - especially the error handling one - if you need more context.

Using Component Store

The first thing we can do to simplify our situation with handling errors, and to address the issue with potentially triggering the source observable twice by subscribing to the error stream, is to incorporate @ngrx/component-store.

As is usually the case, Component Store can make our lives a lot easier when we are trying to code reactively. If you need some additional context for Component Store you can check out the resources below:

Let's see what Component Store can do for us in a situation where we have a stream from a getUser method, and we want to be able to handle errors on that stream. Here is what I set up for this example (for full context and how everything pieces together, make sure to check out the source code):

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { EMPTY, pipe } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators';
import { UserService } from '../shared/data-access/user/user.service';

interface HomeState {
  user: string;
  userError: string;
}

@Injectable()
export class HomeStore extends ComponentStore<HomeState> {
  readonly user$ = this.select((state) => state.user);
  readonly userError$ = this.select((state) => state.userError);

  loadUser = this.effect<void>(
    pipe(
      switchMap(() =>
        this.userService.getUser().pipe(
          tap({
            next: (user) => this.patchState({ user }),
            error: (userError) => this.patchState({ userError }),
          }),
          catchError(() => EMPTY)
        )
      )
    )
  );

  constructor(private userService: UserService) {
    super({
      user: null,
      userError: null,
    });
  }
}

Most of this is just a standard Component Store set up. We have our state - a user and the userError - we have selectors to select that state, and we initialise it to be null in the constructor.

The interesting part is this effect:

  loadUser = this.effect<void>(
    pipe(
      switchMap(() =>
        this.userService.getUser().pipe(
          tap({
            next: (user) => this.patchState({ user }),
            error: (userError) => this.patchState({ userError }),
          }),
          catchError(() => EMPTY)
        )
      )
    )
  );

We will call this from our home page to load the user:

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
  providers: [HomeStore],
})
export class HomePage implements OnInit {
  user$ = this.homeStore.user$;
  userError$ = this.homeStore.userError$;

  constructor(public homeStore: HomeStore) {}

  ngOnInit() {
    this.homeStore.loadUser();
  }
}

I won't get into explaining how Component Store effects work in this tutorial, but the general idea here is that we call the effect, it will switch to the getUser observable stream from the service, and then we will update our state with whatever it emits (and if it errors we set the error state).

In our home page, we make sure to trigger the effect, and then we just grab our two streams - the data stream and the error stream - from the store.

From here everything could just be the same as in the initial error handling example: our template uses the async pipe to subscribe to both of these streams, and if the error stream emits it is going to display an error instead of the loading state or the user.

Using a Dumb Component

...But, I've also made an additional optimisation here. In the template, I now have a separate user-card dumb component that just takes in values from the two streams as standard synchronous inputs:

  <app-user-card
    [user]="user$ | async"
    [err]="userError$ | async"
  ></app-user-card>

If we take a look at that user-card component, you can see we just have some logic in the template for deciding how to display the data and errors:

<ng-container *ngIf="user || err; else loading">
  <ion-card *ngIf="user && !err">
    <ion-card-content>
      <p>{{ user }}</p>
    </ion-card-content>
  </ion-card>
  <ion-note *ngIf="err"> {{ err }} </ion-note>
</ng-container>

You might change this depending on what exactly you want, but this is how I have set the template up to behave:

  • In the case of a successful emission from the stream, we just display the loading template until the stream emits, then we display the data.
  • In the case of the stream erroring, we will remove the loading template and display the error.
  • In the case of the stream erroring after already having emitted multiple values, I remove the previously emitted value from the template and display the error.

Summary

In my opinion, the end result is super clean and nice to work with, and I much prefer this compared to a more imperative approach to error handling without the async pipe - subscribing to the stream in your class and setting data from the stream on class members for example.

Of course, this is just one way to handle it - my main point is that the async pipe does not need to make handling errors difficult or messy (in fact, I think it makes it much nicer!).

If you enjoyed this article, feel free to share it with others!