The RxJava2 Default Error Handler
Or: “why is RxJava crashing my application when I have an
We are all good developers and handle
onError on our RxJava streams. That means that our streams are all crash-proof, right?
We will look at a few examples in which our applications can crash due to an RxJava stream throwing an exception even if we have implemented
onError correctly. But first, let’s look at what these types of issues look like.
The global error handler
RxJavaPlugins.onError is RxJava’s global error handler for any exceptions that can’t be delivered to a subscriber. By default, errors sent to the global error handler will be thrown, and thus your application will crash.
The release notes for RxJava 2.0.6 summarize this behavior (and the reasoning behind it):
One of the design goals of 2.x was that no errors can be lost. Sometimes, the sequence ends or gets cancelled before the source could emit an
onErrorwhich has nowhere to go at that point and gets routed to the
If RxJava didn’t have a global error handler, these exceptions would occur silently, and you would be blissfully ignorant of potential issues in your code.
As of RxJava 2.0.6,
RxJavaPlugins.onError tries to be smart and separate library/implementation bugs from situations in which an error is undeliverable. It will leave exceptions categorized as “bugs” alone and rethrow those exceptions as-is. RxJava wraps any other exceptions in an
UndeliverableException and throws that. You can see the full logic for determining whether an exception is a bug here.
One of the most common exceptions that developers who are new to RxJava encounter is
OnErrorNotImplementedException. This is the familiar exception that you receive if an observable throws an exception and you have not implemented
onError in your subscriber.
OnErrorNotImplementedException is one of the “bug” cases that RxJava does not wrap in an
UndeliverableException, since it generally means that you simply forgot to add an
onError handler but the observable does have a subscriber.
Since most of the “bug” cases are fairly easy to spot and correct, we won’t spend any more time looking at them. The exceptions that RxJava wraps in an
UndeliverableException are a little more interesting because it might not be obvious why the exception can’t be delivered.
As the name may imply, RxJava throws an
UndeliverableException when it cannot deliver an
onError signal. The cases in which this can happen vary based on what your observables and subscribers are doing, and we will look at two examples shortly. For now, suffice it so say that an error signal is undeliverable if there is no subscriber that RxJava can deliver the error to.
The first scenario in which I encountered an
UndeliverableException was with the
zipWith operator. Consider the following code:
We zip together two observables that will each emit an
onError signal. What do we expect to happen? We might assume that our
onError will be twice. However, the reactive streams specification states:
Once a terminal state has been signaled (
onComplete) it is REQUIRED that no further signals occur.
onError is called, it can’t be called a second time. What happens to the second error signal? It is delivered to
One easy way to get into this situation is to zip together two observables that perform network operations (e.g. observable Retrofit calls). If both calls fails (perhaps because there is no internet connection), both observables will emit errors. One error will go to your subscriber’s
onError and the other will go to
Example two: ConnectableObservable with no subscriber
ConnectableObservable can also be a source of an
UndeliverableException. Remember that a
ConnectableObservable will emit items if it is connected, regardless of whether or not there are any subscribers. If the
ConnectableObservable attempts to emit an error signal when it has no subscribers, that error will go to the global error handler.
Here’s an example of some fairly innocent-looking code that can be problematic:
someApi.retrofitCall() emits an error (e.g. because the user has no internet connection), the application will crash because the network error will be delivered to the RxJava global error handler.
This might seem like a contrived example, but it can be easy to get into a situation in which your
ConnectableObservable is still connected but has no subscribers. I ran into this with
autoConnect() on an API call.
autoConnect() does not automatically disconnect the source observable. I unsubscribed from the observable when my Activity was stopped, but if the API call later came back with an error the application would crash with an
Handling the errors
So how do we deal with these exceptions?
The first step if you are seeing these types of crashes is to identify what is causing them. Ideally you want to fix exceptions that are sent to the global RxJava error handler at their source.
One solution for the
zipWith example above is to take one (or both) of the observables and apply one of the error handling operators. For example, you could use
onErrorReturn to provide a default value.
ConnectableObservable example is easy to fix- just make sure to disconnect when your subscribers unsubscribe.
autoConnect for example has an override that takes in a function that can handle the connection appropriately (see more about
Another option for handling errors sent to the global error handler is to set your own error handler.
RxJavaPlugins.setErrorHandler(Consumer<Throwable>) is a hook for doing just that. If it is appropriate for your use case, you can use this intercept all errors sent to
RxJavaPlugins.onError and handle them as you see fit. This is fairly heavy handed though- remember that this is a global handler for all the RxJava streams in your application.
Finally, if you are
create()ing your Observables, you can call
emitter.tryOnError() instead of
emitter.onError(). This will only emit the error if the stream hasn’t terminated and is still subscribed to. Note that this method is experimental though.
The moral of the story is that you cannot assume that having an
onError callback will make your RxJava streams crash-proof. You need to be aware of situations in which error signals might not be deliverable to a subscriber, and make sure to handle those scenarios gracefully.