đź‘‹ Hey there!

I'm Josh Morony, and this website is a collection of my free content centered around creating modern Angular applications using the latest Angular features, and concepts like reactive and declarative code.

Tutorial hero
Lesson icon

Handling Errors Reactively with the Async Pipe in Angular

Originally published February 09, 2022 Time 5 mins

In a recent video, we discussed using the async pipe to automatically subscribe to and pull out data from a stream and dynamically displaying a loading template while waiting for that stream to emit. This article will focus on handling errors with RxJS streams and the async pipe in a reactive way, providing a simple and clean method for error handling.

This article is based on the following video:

Step 1: Understand the Current Scenario

We’ve already seen the success case for using async pipes. Now let’s take a look at what happens when the stream errors. We have a service set up with a few different methods:

  1. The first method returns a single value in a stream successfully.
  2. The second method immediately throws an error.
  3. The third method emits some values successfully over time, but on the third value, it will throw an error.
  getUser() {
    return of('Josh').pipe(delay(2000));
  }

  getUserWithError() {
    return of('Josh').pipe(
      delay(2000),
      tap(() => {
        throw new Error('Could not fetch user');
      })
    );
  }

  getTemporalUser() {
    return of('Josh', 'Greg', 'oops', 'Josh').pipe(
      concatMap((user) => of(user).pipe(delay(2000))),
      tap((user) => {
        if (user === 'oops') {
          throw new Error('Could not fetch user');
        }
      })
    );
  }

For now, let’s switch to using the getUserWithError method and see what happens:

export class HomePage {
  user$ = this.userService.getUserWithError();

  // ...snip
}
<div *ngIf="user$ | async as user; else loading">{{ user }}</div>

<ng-template #loading> Loading... </ng-template>

When we do this, we will just get an eternal loading display because the observable stream has errored. Now let’s look at how we can deal with this.

Step 2: Create a Separate Error Stream

The general idea is to create a separate stream to deal with errors and then utilize that in our template as well. When a stream errors, it is destroyed. We want to create an error stream that emits a value when it errors.

To do this, we can use the catchError operator, which catches when the stream errors and allows us to provide a replacement observable stream to subscribe to instead. The original stream is still destroyed, but now we are returning a new observable stream using of and providing the error as a value for that stream to emit. We can then utilize this value in our template.

user$ = this.userService.getUser();
userError$ = this.user$.pipe(catchError((err) => of(err)));

In our template, we can check for this error stream and display the error if there is one.

The problem we have, though, is that this isn’t just going to emit errors; it will also emit all the normal values of the stream. That’s why we also have the ignoreElements operator:

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

This will ignore all values except errors and completion.

Step 3: Update the Loading Template

Now we need to update our loading template to handle errors. We have moved the entire view model up into an ng-container, which handles subscribing to everything with the async pipe. We need this ngIf to set up these values with the async pipe, but the If functionality isn’t relevant here. This will always evaluate to true; we just need it to set up this object to use.

<!-- some.component.html -->
<ng-container *ngIf="{user: user$ | async, error: userError$ | async} as data">
  <ng-container *ngIf="data.user; else loading">
    <!-- display user data -->
  </ng-container>
  <ng-template #loading>
    <div *ngIf="!data.error">
      <!-- display loading template -->
    </div>
    <div *ngIf="data.error">
      <!-- display error message -->
    </div>
  </ng-template>
</ng-container>

We have added an additional check within the loading template so that if the userError stream has emitted, we won’t continue to show the loading UI.

Step 4: Handle Temporal Values

Now let’s see how our solution deals with temporal values, that is, values emitted over time.

// some.service.ts
getUserWithTemporalError(): Observable<User> {
  // code that emits values over time and throws an error at some point
}

You will see that it works, but it’s a bit awkward because the previous successful users continue to show, while we also have our error message indicating that the current user failed to be pulled in. If we want, we can add another ngIf to the user paragraph to remove it from the DOM when there is an error, so we aren’t displaying old values.

<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>

Remember that when a stream errors, it is destroyed, so we won’t continue to get the remaining values from this stream after the error.

Conclusion

We now have a nice way to deal with errors with streams and the async pipe in a reactive way. Keep in mind that this approach is highly context-dependent. In a real-world scenario, you would likely abstract some of the template logic out into separate presentational components.

Learn to build modern Angular apps with my course