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
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
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'))
Building a strategy
I have a plan!
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!
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)
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
)
)
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
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
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 JSfinally
. It lets you run a function upon stream termination, regardless whether it has completed or failedtimeout
andswitchMap
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 tasteobserveOn
— 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