Thursday, 21 September, 2017 UTC


Summary

Creating complex observable pipelines is all good, but how do you effectively handle errors within them? Let’s go over some of the basics here with the catch, finally, retry and retryWhen operators.
Observer’s Error Callback
At its most basic, observers take an error callback to receive any unhandled errors in an observable stream. For example, here our observable fails and an error message is printed to the console:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }); obs$.subscribe(value => { console.log(value); }, err => { console.error('Oops:', err.message); }, () => { console.log(`We're done here!`); });
And here’s what gets printed in the console:
 0 1 2 3 Oops: too high! 
The Catch Operator
Having unhandled errors propagated to the observer should be a last resort, because we can use the catch operator to deal with errors as they happen in the stream. Catch should return another observable or throw again to be handled by the next catch operator or the observer’s error handler if there’s no additional catch operator.
Here, for example, we return an observable of the value 3:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }) .catch(error => { return Rx.Observable.of(3); }); obs$.subscribe(value => { console.log(value); }, err => { console.error('Oops:', err.message); }, () => { console.log(`We're done here!`); });
Here’s what we get at the console. Notice how our main observable stream completes after the observable returned using catch completes:
 0 1 2 3 3 We're done here! 
A stream can have as many catch operators as needed, and it's often a good idea to have a catch close to a step in the stream that might fail.

If you want the stream to just complete without returning any value, you can return an empty observable:
.catch(error => { return Rx.Observable.empty(); })
Alternatively, if you want the observable to keep hanging and prevent completion, you can return a never observable:
.catch(error => { return Rx.Observable.never(); })

Returning the source observable

Catch can also take a 2nd argument, which is the source observable. If you return this source, the observable will effectively restart all over again and retry:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }) .catch((error, source$) => { return source$; })
This is what we get:
 0 1 2 3 0 1 2 3 0 1 2 3 ... 
You'll want to be careful and only return the source observable when errors are intermittent. Otherwise if the stream continues failing you'll create an infinite loop. For more flexible retrying mechanisms, see below about retry and retryWhen
Finally
You can use the finally operator to run an operation no matter if an observable completes successfully or errors-out. This can be useful to clean-up in the case of an unhandled error. The callback function provided to finally will always run. Here’s a simple example:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }) .finally(() => { console.log('Goodbye!'); }); obs$.subscribe(value => { console.log(value); }, err => { console.error('Oops:', err.message); }, () => { console.log(`We're done here!`); });
This ouputs the following:
 0 1 2 3 Oops: too high! Goodbye! 
Retrying

The retry operator

You can use the retry operator to retry an observable stream from the beginning. Without an argument, it will retry indefinitely, and with an argument passed-in, it’ll retry for the specified amount of times.
In the following example, we retry 2 times, so our observable sequence runs for a total of 3 times before finally propagating to the observer’s error handler:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }) .retry(2) obs$.subscribe(value => { console.log(value); }, err => { console.error('Oops:', err.message); }, () => { console.log(`We're done here!`); });
Here’s the outputted result:
 0 1 2 3 0 1 2 3 0 1 2 3 Oops: too high! 
You can also add a catch right after a retry to catch an error after a retry was unsuccessful:
.retry(1) .catch(error => { return Rx.Observable.of(777); });
 0 1 2 3 0 1 2 3 777 We're done here! 

The retryWhen operator

Using the retry operator is all well and good, but often we want to retry fetching data from our backend, and if it just failed, we probably want to give it a little time before retrying again and taxing the server unnecessarily. The retryWhen operator allows us to do just that. retryWhen takes an observable of errors, and you can return that sequence with an additional delay to space-out the retries.
Here we wait for 500ms between retries:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }) .retryWhen(error$ => { return error$.delay(500); });

The above code will retry forever if the error keep happening. To retry for a set amount of times, you can use the scan operator to keep track of how many retries have been made and throw the error further down the chain if the amount of retries exceeds a certain number.
Here on the 4th retry, we’ll give up and let the error propagate to the observer:
const obs$ = Rx.Observable .interval(500) .map(value => { if (value > 3) { throw new Error('too high!'); } else { return value; } }) .retryWhen(error$ => { return error$.scan((count, currentErr) => { if (count > 3) { throw currentErr; } else { return count += 1; } }, 0); });