Error handling in RxJS

or how to fail not with Observables

As developers we tend to focus on happy paths for our apps, often neglecting its error prone parts, be it calls to a server or to a 3rd party API. In this article I want to give a quick overview of error handling in RxJS with a bunch of marble diagrams explaining what’s happening. I hope, the following examples and my mumbling will help you shorten the gap between our natural desire to pursue new features and our moral obligations to provide smoothest experience to our users. Lets go!

What happens when an error occurs on an RxJS stream?

the first thing to ask

simplest throwError example marble diagram

Well, basically, it fails. The error is propagated through the operators chain until it gets handled. If no operator handles it — then error is raised to the subscriber, effectively terminating the stream. No delay or filter will affect it

timer(5).pipe(

switchMap(() => throwError('Error!')),

delay(5) // no effect on errors

)

Here’s a throwError example you can play with

Keep scrolling to see how you can handle it!

Graceful error handling

try..catch in RxJS

example of catchError turning error into a value

The simplest way to handle an error on a stream — is to turn an error into another stream. Another stream — another life, eh? catchError will let you substitute an error with a stream of your choice. It is useful when you have a backup source, want to suppress error or substitute it.

Here we replace an error with a single value stream

catchError(err => of('oh'))

A catchError example.

Building a strategy

I have a plan!

onErrorResumeNext example with two alternative streams: failed timer and fine timer

If we have alternatives to our source stream, e.g. having several providers for weather forecast, — we can feed this fallback list to an onErrorResumeNext operator. It will subscribe to the first source in the list and if this source fails — it will subscribe to the next one. One by one going through the list, till any attempt completes successfully or when we’re out of alternatives

onErrorResumeNext(

failStream$,

fineStream$

)

An onErrorResumeNext example with timers.

Retrying failed attempts

we wont give up that easily!

a retry example. three retries, last attempt successful

Sometimes we just know that the door will open if knocked at enough times. Our data server might fail due to an absent internet connection. So retry operator will come handy if we want to create a stream, that wont give up until it made certain number of attempts

retry(3)

A retry example.

Retrying with a delay

we wont give up that easily… yet we need some rest!

Not always immediate retry will give us results. The server that we’ve DDoSed with relentless retry() might need some cool down time. retryWhen lets us define exactly when to retry. We get a stream of errors and return a stream of retry notifications. Once a value is emitted on the returned stream — a retry is initialized

retryWhen(error$ =>

error$.pipe(

delay(100) // retry in 100ms

)

)

A retryWhen example.

NOTE: there’s a catch with retryWhen operator. Resulting observable will complete with its retry notification observable. Which means that retryWhen(err$ => err$.take(1)) will init a retry and then will immediately complete. Even if this attempt would’ve produced values. My advice to properly limit retries on error — use switchMap((error, index) => ...) to switch based on index to either of(error) to retry, or on throwError to propagate the error. See this limited attempts retryWhen example for details

Retrying with an exponential backoff

a leveled up delayed retry

exponential backoff example

If errors keep occurring upon retrying — we might want to slow down and delay each attempt further and further, increasing the gap between fails and retries

retryWhen(error$ =>

error$.pipe(

delayWhen((_, i) => timer(i * i * 10))

// will retry with

// 0, 10, 40, 90, 160 ms delays

)

)

NOTE: in real life you would usually want to reset your back off delay once stream has started pushing values. E.g. when the internet connection has been reestablished. To achieve that we might store some flag in the JS scope and reset it in a tap . E.g. tap(()=>{ restoredConnection = true; }) . Actually, theres a bunch of libraries that already implement this behavior, e.g. https://github.com/alex-okrushko/backoff-rxjs

Magical transfiguration of errors

the two strongest spells

delayed and remapped error example, using materialize/dematerialize pair

As you know, errors and completions are propagated immediately, and delay wont delay them, and map wont map them. To manipulate errors and completions — we can turn them into ordinary value emissions, using materialize .

Then we’ll be able to delay , map , or whatever comes to our wicked minds.

After we’re done having fun — dematerialize brings things back to normal. Error notifications will turn into errors, complete notifications into completions, golden carriages into pumpkins

materialize(),

delay(10),

// turn an error into a value emission:

map(n => new Notification('N', n.error, undefined)),

dematerialize()

A materialize-dematerialize example.

Worth mentioning

Just a couple of things that didn’t fit in this article, yet definitely worth mentioning: