Improving RxJava stack traces with RxDogTag

Jamie Lynch

RxJava has become a widely used reactive programming library among developers on Android, as it greatly improves the concurrency experience and allows for applications to be written in a reactive and functional manner. However, one disadvantage of RxJava is that when an error occurs, stack traces may not be as useful when debugging in Bugsnag. The good news is that the RxDogTag library can make stack traces more useful, which provides for a more pleasant experience when debugging errors.

Fetching JSON from a REST API with RxJava

Let’s consider a simple task that nearly every Android app needs to achieve: asynchronously consuming JSON from a REST API and then updating the user interface on the main thread. With the Retrofit networking library, it’s possible to use RxJava’s Observable API to perform a network request, deserialize and map the JSON response, then update the UI, all using the following code:

val observable: Observable<MovieList> = restApiService.fetchMovieList()
observable.map { list -> UiModel(list) } // map the response onto another object
    .observeOn(AndroidSchedulers.mainThread()) // observe events on main thread
    .subscribe { uiModel -> movieCount.text = “There are $uiModel.count movies” }

If you’re not familiar with RxJava reactive programming, Dan Lew’s blog series Grokking RxJava is an excellent starting point for learning the fundamentals of RxJava.

To summarise, #fetchMovieList() deserializes JSON from a REST API and then emits a #MovieList object to an #Observable. Once an event has been emitted, it can be transformed by operators which form an observable chain. In this example, we convert the object to the type #UiModel, but there are many operators so the possibilities for transformation are almost endless. After the data has been transformed, a subscriber will be informed, and they can react to the event — in this case by updating the text displayed on a #TextView.

The key thing to grasp is that when an #Observable emits an event, it will likely call either #onNext() or #onError(), which are both methods that subscriber can implement. In our example above we’ve assumed that the API response will always be mapped to a #UiModel correctly and that #onNext() will always be called when fetching data.

However, if a fatal error occurred in that Observable chain, then #onError() will be invoked and the Observable will terminate. If #onError() wasn’t implemented at all by the subscriber, then this will have an interesting effect on the following code:

restApiService.fetchMovieList()
    .map { list ->  null } // whoops - there was a bug in our mapping logic!
    .subscribe { ... }

That’s right, our app will crash. This is due to the fact that RxJava2 does not accept null in an Observable, meaning #onError() will be invoked. As we’ve not implemented an #onError() method for this subscription, RxJava’s default behaviour is to throw an Exception that will terminate the process. Therefore the stack trace for that Exception will look something like this:

Yikes! From just looking at the exception’s stack trace, you might have a hard time determining which Observable chain has a bug, particularly if you have a large app where there may be hundreds of different subscriptions. This is where the RxDogTag library comes in.

RxDogTag to the rescue

RxDogTag is an open-source library released by Uber that enhances all RxJava stack traces for subscriptions that don’t have a custom #onError() implementation. After installing the library in your app, the stack trace contains much more useful information about what went wrong:

The key information here is the inferred subscription point, as it will tell us where we originally subscribed to the Observable. In this case we can see that there is a bug in #subscribeOnMainThread() within our #MainActivity, which is exactly where we started listening to our buggy Observable. This is what RxDogTag has added to our stack trace, which makes things much easier to debug.

An added bonus is that this also improves error grouping in Bugsnag. Error grouping groups separate incidences of similar stack traces together, in order to display them as one individual event. We achieve this by inspecting the top stack frames, which in the case of RxDogTag is now the inferred subscription point, meaning the errors will no longer be grouped on the #OnErrorNotImplementedException and will avoid ‘over-grouping’.

How does the RxDogTag library work?

Due to the asynchronous nature of RxJava stack traces are usually not that useful, as any error within an Observable stream will usually be on a different thread to the subscriber, meaning the original subscription call site won’t be present in the stack trace. 

RxDogTag achieves this by registering an RxJavaPlugin, which eagerly creates a #Throwable for every Observer that does not have a custom error handler defined. This plugin decorates the existing observer and whenever an error occurs, introspects the stack frames from the previously created #Throwable in an attempt to find the inferred subscription point. Finally, RxDogTag mutates the stack trace to add a frame for the inferred subscription point, before allowing program execution to continue as normal so that crash reporting services can record this information.

That’s a lot to take in, and it may just be simplest to read the code to get a full understanding of how the RxDogTag library works. At the time of writing, the library is around 1k LOC, with RxDogTag.java containing the bulk of the introspection logic.

One advantage of this approach is that it avoids enabling assembly tracking, which can become quite costly in terms of performance if your application is regularly creating Observables. Instead, RxDogTag captures only where the subscription occurred, rather than reconstructing the whole operator chain. As an additional optimisation, stack traces are only enhanced when the user has not specified their own custom #onError() implementation, as in this case it is probable that the user has already logged diagnostic details or handled the error in some other way.

Installing RxDogTag

RxDogTag is very simple to install. After following the Github setup instructions it’s just a case of calling #RxDogTag.install() early in your project’s application lifecycle, which will configure RxJava by installing a plugin that enhances the stack traces reported to Bugsnag.

If you use RxJava and aren’t satisfied with the quality of the stack traces in production, give RxDogTag a try today!

Share