Search code examples
androidrx-javarx-kotlin

RuntimeException handling best practices


RuntimeExceptions are supposed to indicate programming error and I want my application to crash when something inside my observables throws RuntimeException.

What is the best way to do this? Right now I'm considering this solution (it's Kotlin, but I hope it's understandable)

fun <T> Observable<T>.subscribeCrashOnRuntimeException(onNext: (T) -> Unit, onError: (Throwable) -> Unit) {
  this.subscribe({
    onNext(it)
  }, { e ->
    if (e is RuntimeException) {
      throw e
    } else {
      onError(e)
    }
  })
}

fun usageExample() {
  val observable = Observable.just(1)
  observable.subscribeCrashOnRuntimeExceptions(
    { next -> Log.d("TAG", "next: $next") },
    { e -> Log.d("TAG", "error: $e") }
  )
}

But I have doubts about it. For example it is hard to occasionally "catch" specific RuntimeExceptions with this solution. Perhaps there is a well known way to deal with situation that I just don't know how to google?


Solution

  • I don't think there should be much difference in handling runtime (aka unchecked) or regular (aka checked) exceptions. Both are widely used these days and could be either recoverable or not depending on particular situation.

    The reactive ways of handling errors are:

    1. via onErrorResumeNext or onErrorReturn operators; these allow to inspect the error and possibly recover from it
    2. via retry* family of operators; these allow to inspect the error and possibly recover from it by means of re-subscribing (for example retry network call)
    3. via onError callback of your subscribers; by the way in case you do not supply such a callback the error will be re-thrown in regular Java fashion, so your program will crash

    Related topic: How to handle different kinds of errors in Retrofit Rx onError without ugly instanceof

    Also note the drawbacks of throwing exceptions in regular Java way:

    • the call stack during message processing is different from the call stack when you define message processing rules; this means it could be quite hard to catch such exceptions, as well as to interpret the stack trace
    • exceptions caught by schedulers might not lead to program termination; i.e. your program might end up hanging in broken state

    Sample code

    Observable.fromCallable(() -> {
        ...
        if (ok) return "Success!";
        else throw new RuntimeException("Failure at source");
    })
    .map(s -> {
        ... processing is bypassed in case of an error
    })
    .map(s -> {
        ...
        if (...) return s.upperCase();
        else throw new RuntimeException("Failure during processing");
    })
    .onErrorReturn(e -> {
        if (e.getMessage().contains("processing"))
            return "Recovered";
        throw Exceptions.propagate(e); // let it continue as an error
    })
    .subscribe(s -> println("got result: " + s),
               e -> println("got error: " + e);
    

    All exceptions get caught by RxJava and passed along the defined route.

    onError* operators act like intermediate catch blocks.

    Subscriber's onError callback acts like top-level catch block.

    More links on the subject: