RxJS Operators: catchError

RxJS Operators: catchError

RxJS is a JavaScript library that enables the creation of asynchronous and event-based programs. The main type is the Observable and a suite of functions and operators are provided to make it easier to work with the data coming through. This series will detail those operators (and a few stand-alone functions) and provide examples of their use.

In this post, we’re going to cover the catchError operator.

What does it do?

This operator, as the name suggests, is used to catch errors that are thrown from our observable. This gives us the ability to add special handling for errors and, more importantly, control what happens with our subscription.

Example

Here we’re going to use the interval function and the map operator. The interval function will provide us with an observable outputting incrementing numbers. We’ll be using the map operator to throw an error when the value reaches 10:

import { catchError, interval, map, of } from 'rxjs';

interval(1000)
    .pipe(
        map(count => {
            if (count === 10) {
                throw new Error('Error thrown at count of 10');
            }

            return count;
        }),
        catchError((error: Error) => {
            return of('Error handled: ' + error.message)
        }))
    .subscribe(x => {
        console.log(x);
    });

This gives us the output:

0
1
2
3
4
5
6
7
8
9
Error handled: Error thrown at count of 10

We’ll see the program end at this point. This is due to a quirk of using interval rather than being expected behaviour. When the subscription to interval ends it will stop outputting values and by default an error being thrown will end the subscription.

What do we do if we want the subscription to continue? Well, the catchError operator has us covered. In the example above we’ve passed catchError a function with a single parameter, error. The optional second parameter provides us with a reference to the originally subscribed observable.

Let’s see an example of this. We’re going to create an observable for this one to avoid the quirks of interval:

import { Observable, Subject, catchError, map } from 'rxjs';

const provider$ = new Subject<number>();
let count = 0;

provider$
    .pipe(
        map((x) => {
            if ((x % 5) === 0) {
                throw new Error('Error thrown at multiple of 5');
            }

            return x;
        }),
        catchError((error: Error, caught: Observable<number>) => {
            console.error('Error handled: ' + error.message);
            return caught;
        }))
    .subscribe(x => {
        console.log(x);
    });

setInterval(() => {
    count++;

    provider$.next(count);
}, 1000);

It looks like there’s a lot more going on here, but we’ve just implemented our own version of interval. Now the map operator will throw an error every time we reach a multiple of 5. The important change is in the catchError handling:

catchError((error: Error, caught: Observable<number>) => {
    console.error('Error handled: ' + error.message);
    return caught;
}))

Our error handling now takes caught(the original observable), logs an error, and returns caught. This will just force our pipeline to resubscribe on error. So now we get:

1
2
3
4
Error handled: Error thrown at multiple of 5
6
7
8
9
Error handled: Error thrown at multiple of 5

As we see, each time an error is thrown we just resubscribe and wait for the next value. There are a couple of things to watch out for with this approach though:

  • caught will return the original subscription with all the operators you’ve added. This may be what you’re looking for, but if there’s something in your pipeline throwing the error (like we have above) it’s going to keep happening

  • If you’re using a BehaviorSubject the resubscribe is going to attempt to reprocess the last value that was sent from the observable. Like the first point, if you’re pipeline caused this error you risk it happening again.

So just bear in mind the observable you’re returning. You don’t want to end up with:

Error handled: Error thrown at multiple of 5
Error handled: Error thrown at multiple of 5
Error handled: Error thrown at multiple of 5
Error handled: Error thrown at multiple of 5
Error handled: Error thrown at multiple of 5
Error handled: Error thrown at multiple of 5

Because the programming is repeatedly attempting to process a bad value.

The source code for this example is available on GitHub: