Towards fixing RxJava stacktrace

Alexander Shafir
Feb 8, 2021 · 3 min read

Unreadable RxJava error stacktrace is a known problem. Chances are you also faced cryptic crash log from your QA team and was scratching your head for hours before finding solution or even abandoning said crash altogether.

Relevant discussion on RxJava Github.

This article is intended for both Java and Android devs. Part of snippets are currently in Kotlin, I am in process of migrating it to Java (to be compatible with both worlds). Ignore code irrelevant for your platform.

TL;DR

  1. Use RxDogTag safely.
  2. Prefer to use RxJavaAssembly in debug as it is performance-costly.
  3. You can use assembly tracking and RxDogTag together.
  4. I created helper class that allows you to enable/disable assembly tracking in specific chain (irrespectful of threading), search for “enable/disable” in text. Not compatible with compose that switches observable! Still in beta.
  5. Error swallowing not covered currently.

Take for instance this RxJava3 snippet on Android, that switches threads and produces NPE (Android):

By default, RxJava prints following error stacktrace:

There are three drawbacks:

  1. It does not point to our source code :)
  2. (Android) Printed in plain white, easy to miss in Android Studio.
  3. (Android) Every line starts with “YYYY–MM–DD HH:MM:SS.SSS 10890–10890/com.test.myapplication W/System.err:” that adds noise when re-sending logs (it was omitted here for clarity).

There is also case when RxJava swallows error (e.g. subscribeOn with any Rx scheduler) but I have not touched it yet.

Finding error source

Here we assume onError is not used in subscribe (it will be discussed later).

I present two libraries, way to combine them and my extension for assembly tracking.

1. Subscription tracking

Library: Uber’s RxDogTag

To enable it you need to call

RxDogTag.install()

Implementation: throwing an exception and capturing the stack frame at that point.

This yields following stacktrace:

Pros:

Cons:

  • Only locates subscription point and not error source
  • Not compatible with onError

2. Assembly tracking

Library: akarnokd’s RxJavaExtensions (RxJava2Debug for RxJava2)

To enable it you need to call

RxJavaAssemblyTracking.enable()

Implementation: assembly tracking. Every time you apply operator, new object created — this is called assembly. And on each assembly stacktrace gets captured via Thread.currentThread.getStackTrace which is costly operation.

It means:

  • If you have long chain of operator, you will get performance hit once.
  • If you have re-create chain of operators (like in a function), you will get performance hit each time you instantiate such function.

It yields even longer stacktrace (Android):

It contains two lines from our source code:

at com.test.myapplication.MainActivity.onCreate(MainActivity.kt:25)

— points to map operator (actual error source).

at com.test.myapplication.MainActivity.onCreate(MainActivity.kt:26) 

— points to observeOn (redundant).

Pros:

  • Actual error place (finally!)

Cons:

  • High performance impact
  • Long stacktrace
  • Two error lines
  • Error reason message is far from error source
  • No subscription point

3. Use both libraries together

To get both error source and subscription point.

To enable it you need to call:

RxDogTag.install()RxJavaAssemblyTracking.enable()

Stacktrace (only relevant parts for clarity):

4. Enable/disable assembly tracking in chain with my operators

I created helper class that allows you to enable/disable assembly tracking in specific chains, irrespectful of threading. I called it AssemblySwitch. Currently it is for plain Observables, as I still think on how to improve it. Usage:

AssemblySwitch:

Fixing formatting (Android)

For subscribe w/o onError, you can use RxJavaPlugins.setError {} to intercept errors globally.

Managing onError

We cleared case when we do not have onError set. What if we need to set onError?

E.g.

In this case RxDogTag does not work :( So we need to implement it manually.

import com.test.myapplication.RxErrorHandler.Companion.handleOnError...
.subscribe({}, {
if(...) {} else handleOnError(it)
})

Stacktrace (with both RxDogTag and RxJavaAssemblyTracking enabled):

Helper code:

Note that Log has message limitation that cuts off too long stacktrace (commented out), so you may want to print message line-by-line.

I also have function init here, you can call it on app startup. I created it to have all RxJava logging in one place.

Conclusions

  • In case you have high-load places, use RxDogTag in production, RxJavaExtensions in test setup.
  • Libraries can be used together.
  • Stacktrace styling can be improved.

Feel free to ask questions or provide your thoughts!

The Startup

Get smarter at building your thing. Join The Startup’s +750K followers.