Error handling in RxJS

or how to fail not with Observables

Kostia Palchyk
6 min readMar 21, 2019

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(any_value) 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:

  • finalize operator is similar to JS finally. It lets you run a function upon stream termination, regardless whether it has completed or failed
  • timeout and switchMap will help you throw errors conditionally. Useful when you want to terminate a request taking too long, or have a value that doesn’t fit your consumer. Combine these with above mentioned techniques for better taste
  • observeOn — another powerful magic to create an observable that observes an observable. weird.

Have another example, a different approach or a question — please, share that in the comments section! Your feedback is very valuable!

If you enjoyed reading this article — give a push to the clap button: it will let me understand usefulness of this topic and will help others discover this read. Follow me here on medium and twitter for more updates!

I’m proud that you’ve read so far! Congratulations!

The End

Additionally

I often post about RxJS, you can follow me on twitter to get latest updates:

In my previous article I’ve covered several pausing strategies in RxJS, including buffering, filtering and smart delaying techniques. Take a look if you’ve ever faced that kind of puzzles:

In my new article, I’m comparing RxJS operators: debounce vs throttle vs audit vs sample:

Useful links

Be sure to check this RxJS Playground — the tool I used to compile marble diagrams for this article. I’ve created it to help developers (myself included) explore, understand and explain RxJS streams. Give it a try!

Long read “RxJs Error Handling: Complete Practical Guide” by Angular University

More on backoff in “Power of RxJS when using exponential backoff” by Alex Okrushko

--

--