Errors are first class citizens in reactive programming. The error flow is adopted as a valid case of all observables. This is an essential and great feature but also one of the less understood parts. In this post we explore some of the most common RxJS error handling strategies.

For more error handling example look at this helpful page: learnrxjs.

Observable contract

The following code snippet is typical for RxJS. The subscribe method on the observable http$ has 3 optional handler functions for the value-, error- and complete-flow. These are 3 events an observable can emit. Note that error and complete event are mutually exclusive by the observable contract.

const http$ = this.http.get<Person[]>('/api/persons'); 

http$.subscribe(
    res => console.log('Response', res),
    err => console.log('Error', err),
    () => console.log('Completed')
);

But the above subscribe method has its limitations when it comes to error handling. What if we want to recover from an error? In the next part we discuss some other ways to handle errors.

Operator: catchError

The catchError operator will pass through all value and complete events from the source observable. Only when an error is emitted the operator will return a Replacement Observable.

const http$ = this.http.get<Person[]>('/api/persons');

http$
    .pipe(
        catchError(err => of([]))
    )
    .subscribe(
        res => console.log('Response', res),
        err => console.log('Error', err),
        () => console.log('Completed')
    );

In this example we see that the replacement observable in case of an error is of([]), this creates an observable with only one value: []. In the case that the http call returns an error the result is:

Response []
Completed

No error! The replacement observable is returned instead.

Another strategy could be to rethrow the error. This could be the case when you want to have your own custom error to be handled further on.

const http$ = this.http.get<Person[]>('/api/persons');

http$
    .pipe(
        catchError(err => {
            return throwError('This is a custom error!');
        })
    )
    .subscribe(
        res => console.log('Response', res),
        err => console.log('Error', err),
        () => console.log('Completed')
    );

Operator: finalize

Sometimes a piece of code has to be executed no matter what the case. “Error or not this resource/memory has to be released.” RxJS has the finalize operator that will always be executed. Let’s combine the previous examples in the following.

const http$ = this.http.get<Person[]>('/api/persons');

http$
    .pipe(
        catchError(err => {
            console.log('rethrowing error', err);
            return throwError('This is a custom error!');
        }),
        finalize(() => console.log("first finalize")),
        catchError(err => {
             console.log('provide replacement');
             return of([]);
        }),
        finalize(() => console.log("second finalize"))
    )
    .subscribe(
        res => console.log('Response', res),
        err => console.log('Error', err),
        () => console.log('Completed')
    );

An error of the http observable will output the following. Note that the last finalize is executed after the value handler and complete handler functions.

GET http://localhost:4200/api/persons 500
   (Internal Server Error)
rethrowing error
   (HttpErrorResponse {headers: ... })
provide replacement
first finalize
Response []
Completed
second finalize

Operator: retryWhen

Yet another strategy is to retry. In the case of an error we can catch it and just subscribe to the source observable again. The only question is when to retry. Do we want to retry immediately? Or do we want a small delay? We will create a Notifier Observable to signal when to retry. Take a look at the marble diagram of the retryWhen operator:

rxjs retrywhen marble diagram - RXJS ERROR HANDLING

The first line is the Notifier Observable where the r is a signal to retry. The second line is the source observable that errors. At the bottom we have the result: After each retry event we subscribe again te the source and all events are received again.

The power of this construction is that we can build the notifier observable in whatever way we want. In the next example we will retry every second if the http call emits an error.

const http$ = this.http.get<Person[]>('/api/persons');

http$
    .pipe(
        tap(() => console.log("HTTP request executed")),
        retryWhen(errors => {
            return errors.pipe(
                       tap(() => console.log('retrying...')),
                       delay(1000)
                   );
        })
    )
    .subscribe(
        res => console.log('Response', res),
        err => console.log('Error', err),
        () => console.log('Completed')
    );

Now let’s say the api is temporarily unavailable and after 1 second it is back up. This would be the output:

GET http://localhost:4200/api/persons 500
   (Internal Server Error)
retrying...
HTTP request executed
Response {...}
Completed

Operator: delayWhen

When we want some logic to determine how long the delay should be, we can use the delayWhen operator. The following code snippet will have the same result as the previous. But the setup allows to expand the function as desired.

retryWhen(errors => {
    return errors.pipe(
        tap(() => console.log('retrying...')),
        delayWhen(() => timer(1000))
    );
})

Hopefully you are now able to use these different strategies of RxJS error handling. Handle with care.