My organization’s applications make extensive use of the RxJS library to subscribe to APIs that shuttle data to and from databases and information providers. If you’ve spent any amount of time on the Internet, you are no doubt all-too aware that making calls across the wire are prone to all manner of hiccups. These can stem from network congestion, hardware failure, or application bugs. For those reasons, you need to handle your RxJS subscriptions with care. Part of this includes making sure to close active subscriptions once they are no longer required. The other way to guard against the unexpected is to employ rigorous error handling. Due to their asynchronous nature, the standard try/catch block won’t cut it. Instead, RxJS provides a few of its own mechanisms for gracefully managing the inevitable errors that come with network calls. This article will focus on two of these: Subscribe Callbacks and the catchError operator.
The Subscribe Error Callback
In addition to an Observer or Function, the subscribe() method also accepts two additional Function parameters: one for error handling and a another that executes upon successful completion. You can see them here in VS Code:
Below is an example of an API call that fetches user settings from a data store. Should an error occur, the viewType is set to the default and the error is rethrown:
this.userService.getSettings(userid) .subscribe(userSettings => { this.viewType = userSettings.settings['viewType'] || this.viewSelectionValues.circle; }, error => { this.viewType = this.viewSelectionValues.circle; if (error.status && error.status !== 404) { throw error; } });
In this case, the error handler is actually performing double duty: besides setting the default view type, it also filters out 404 errors, which signify that there were no settings found for that user. This is not a true error because it is expected for new users.
The catchError Operator
The Subscribe Error Callback should be your first choice for RxJS subscription error handling. However, this approach has some limitations. For example, there’s no way to recover from the error or emit a fallback value that replaces the one that we were expecting from the API. For that, there’s the catchError operator. Like most RxJS operators, catchError is really a function that takes in an input Observable, and outputs an Output Observable. Should an error occur, catchError passes the error to the error handling function. That function is then expected to return an Observable which is going to be a replacement Observable for the stream that just errored out.
In this refactored version of our first example, the catchError operator is employed to intercept errors before the subscribe. In the catchError handler function, the error is discarded and a default value is provided. Thus, the error handler is never invoked:
this.userService.getSettings(userid) .pipe( catchError( err => of({settings: this.viewSelectionValues.circle}) ) ) .subscribe(userSettings => { this.viewType = userSettings.settings['viewType'] || this.viewSelectionValues.circle; }, error => console.log('HTTP Error', error));
Rethrowing the Error
Although catchError has the ability to swap out the current observable with a new one, you can still throw an error if you like. To do that, return the results of the throwError() function. You can pass in the original error, or a new, customized one:
this.userService.getSettings(userid) .pipe( catchError(err => { if (error.status && error.status !== 404) { console.log('Error encountered while fetching user settings!', err); return throwError(err); } else { return of({settings: this.viewSelectionValues.circle}); } } ) .subscribe(userSettings => { this.viewType = userSettings.settings['viewType'] || this.viewSelectionValues.circle; };
Retrying after an Error
Although we can’t recover after a stream errors out, there is nothing stopping us from re-subscribing to the stream’s source Observable (i.e. the service). The secret to retrying is the retryWhen operator. It automatically retries an observable on error:
let retryAttempts = 0; this.userService.getSettings(this.userid) .pipe( retryWhen(errors => errors.pipe( delayWhen(() => { console.log(`Retry attempt #${++retryAttempts}`); return timer(2000); }), take(3) ) ) ) .subscribe(userSettings => { this.viewType = userSettings.settings['viewType'] || this.viewSelectionValues.circle; };
retryWhen Extras!
One important thing to keep in mind about the retryWhen Operator, is that the function that defines the Notification Observable is only called once. If you want to try a few times, you can include the take() operator. It’s even more powerful when you combine it with delayWhen(). The latter causes retryWhen to retry after the duration specified via the timer() operator.
Note that you’ll have to import RxJS Objects and operators in order to use them:
import { Observable, timer } from "https://cdn.skypack.dev/rxjs"; import { retryWhen, tap, delayWhen, take } from "https://cdn.skypack.dev/rxjs/operators";
Here’s the demo on Codepen.io. It shows how to use The Subscribe Error Callbacks as well as the retryWhen operator:
You’ll want to open the built-in console to see logging output.
Conclusion
Proper handling of errors is critical to working with asynchronous services over a network due to the sheer number of problems that can arise. Between the RxJS library’s Subscribe Callbacks and the catchError operator, you can choose to gracefully handle an error or let it flow through to alert the user.