2

I feel like I'm not understanding something about RxJava2's error-handling. (btw, I have read RxJava2 observable take throws UndeliverableException and I understand what happens, but not exactly why or if there's a better way to deal with it than what I discuss below. I have also seen RuntimeException thrown and not caught in RxJava2 Single after it has been disposed and it doesn't seem exactly relevant.)

Let's say I have the following, very much simplified example:

val sub = Observable.fromCallable(callableThatCanReturnNull)
                    .subscribe({ println(it) }, { System.err.println(it) })

And the following sequence of events happens, in order:

  1. sub.dispose()
  2. That darn Callable returns null.

Looking at the RxJava2 source, I see this:

@Override
public void subscribeActual(Observer<? super T> s) {
    DeferredScalarDisposable<T> d = new DeferredScalarDisposable<T>(s);
    s.onSubscribe(d);
    if (d.isDisposed()) {
        return;
    }
    T value;
    try {
        value = ObjectHelper.requireNonNull(callable.call(), "Callable returned null");
    } catch (Throwable e) {
        Exceptions.throwIfFatal(e);
        if (!d.isDisposed()) {
            s.onError(e);
        } else {
            RxJavaPlugins.onError(e);
        }
        return;
    }
    d.complete(value);
}

Apparently sometime between the first d.isDisposed() check and the second, d is disposed, and so we hit RxJavaPlugins.onError(e) with e being a NullPointerException (from ObjectHelper.requireNonNull()). If we have not called RxJavaPlugins.setErrorHandler() with something, then the default behavior is to throw a fatal exception that will crash the application. This seems Bad (tm). My current approach is the following:

    GlobalErrorFilter errorFilter = new GlobalErrorFilter(BuildConfig.DEBUG /* alwaysCrash */);
    RxJavaPlugins.setErrorHandler(t -> {
        if (errorFilter.filter(t)) {
            // Crash in some cases
            throw new RuntimeException(t);
        } else {
            // Just log it in others
            Logger.e("RxError", t, "Ignoring uncaught Rx exception: %s", t.getLocalizedMessage());
        }
    });

And my GlobalErrorFilter takes into consideration this fromCallable case and also UndeliverableException. In fact, it looks like this:

class GlobalErrorFilter(private val alwaysCrash: Boolean = false) {

    /**
     * Returns `true` if app should crash; `false` otherwise. Prefers to return `true`.
     */
    fun filter(t: Throwable) = when {
        alwaysCrash -> true
        t is UndeliverableException -> false
        t.localizedMessage.contains("Callable returned null") -> false
        else -> true
    }
}

This feels really hackish. What I want is to tell RxJava that if I have disposed of an observable, I do not care whether (1) it emits new items (2) it completes (3) it onErrors. I just don't care. Is there a way to do that without this global error handler?

AutonomousApps
  • 4,229
  • 4
  • 32
  • 42
  • See `create()` and `ObservableEmitter.tryOnError`. – akarnokd Feb 09 '18 at 18:56
  • 1
    Your suggestion is that I manually create all my observables with `Observable.create()`? How would that help with, e.g., an Observable returned by a Retrofit call? It also seems like kind of a heavy solution, to say "never use the factory methods provided out of the box". – AutonomousApps Feb 09 '18 at 19:53
  • 2
    It helps if you are in control of creating the `Observable`, its overhead is relatively small compared to `fromCallable`. Many blocking APIs throw if the thread gets interrupted but there is usually no standart exception RxJava could filter out for you. So it is either the Retrofit adapter's responsibility to not throw when the flow is disposed or you setup a global error handler and decide what is a crashworthy exception and what is not. – akarnokd Feb 09 '18 at 20:04
  • The issue is that RxJava2 no longer allows `null` to be emitted. That means that using `fromCallable()` with a method that can return `null` is illegal. – Bob Dalgleish Feb 09 '18 at 20:04
  • 1
    What does `null` mean in your context? Missing data or a bug in your routine that is supposed to return a value? With `create`, you can decide to complete. By the way [Maybe.fromCallable](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Maybe.html#fromCallable-java.util.concurrent.Callable-) does treat `null`s as empty indicators. – akarnokd Feb 09 '18 at 21:32
  • @akarnokd I don't want to get too bogged down in the `fromCallable` case -- it was just an example. However, I'd say that in my context, `null` actually represents a poor understanding of the Rx contract -- we shouldn't have been returning a null value. _However_ however, `Maybe.fromCallable` might be a good drop-in replacement -- thanks! Thanks very much for your comments! I'll experiment with adjusting our Retrofit adapters to check `isDisposed()` before emitting anything, and update based on those results. – AutonomousApps Feb 09 '18 at 21:38

0 Answers0