Preface
Empiricism kills people. We know that rxjava provides a very good error handling mechanism, so errors in chain calls will eventually be received by onError in subscribe. But is that really the case?
Example
public static void test1(){ mDisposable = Flowable.create(new FlowableOnSubscribe<Object>() { @Override public void subscribe(FlowableEmitter<Object> emitter) throws Exception { //For example, make a network request try { Thread.sleep(5000); }catch (Exception e){} //TODO boolean b = true; // Simulate request status if(b){ emitter.onNext(new Object()); emitter.onComplete(); }else { emitter.onError(new RuntimeException("no data")); } } }, BackpressureStrategy.BUFFER).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Object>() { @Override public void accept(Object o) throws Exception { //TODO } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { throwable.printStackTrace(); } }); }
The code is very simple, which is to simulate a network request. If onNext,onComplete are called correctly, and if onError is called incorrectly and an error is thrown, the error will be caught by Consumer < Throwable> in subscribe. There is no problem with the whole process.
What about the following code?
public static void stopTest1(){ if(mDisposable!=null && !mDisposable.isDisposed()){ mDisposable.dispose(); } }
At this point the situation began to get a little complicated. Let's have a stab.
Scenario 1: We call stopTest1 after emitter throws data, and the interface returns correct (b = true)
Scenario 2: We call stopTest1 after emitter throws data, and the interface returns correct (b = false)
These two situations are the longest, and we have been using them all the time without any problems.
Case 3: We call stopTest1 before emitter throws data (that is, before onComplete above) and the interface returns correct (b = true)
Case 4: We call stopTest1 after emitter throws data (before onError above) and the interface returns correct (b = false)
For case three, there is no problem. But in case 4, app crash happened.
09-27 17:09:09.213 4395-4577/com.guardz.test E/AndroidRuntime: FATAL EXCEPTION: RxNewThreadScheduler-2 Process: com.guardz.test, PID: 4395 io.reactivex.exceptions.UndeliverableException: java.lang.RuntimeException: no data at io.reactivex.plugins.RxJavaPlugins.onError(RxJavaPlugins.java:366) at io.reactivex.internal.operators.flowable.FlowableCreate$BaseEmitter.onError(FlowableCreate.java:271) at com.guardz.test.Utils$3.subscribe(Utils.java:50) at io.reactivex.internal.operators.flowable.FlowableCreate.subscribeActual(FlowableCreate.java:72) at io.reactivex.Flowable.subscribe(Flowable.java:13234) at io.reactivex.Flowable.subscribe(Flowable.java:13180) at io.reactivex.internal.operators.flowable.FlowableSubscribeOn$SubscribeOnSubscriber.run(FlowableSubscribeOn.java:82) at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66) at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:764)
Errors generated in our onError are throw n out directly.
Cause analysis
Let's see what we do when we call onError.
First, Flowable.create creates a FlowableCreate object. When we call subscribe, we actually call subscribeActual, and then it further calls the subscribe method of the FlowableOnSubscribe object that I created as the imported FlowableOnSubscribe object. (Tucao, it's a lot of subscribe!
backpressure is an example of BackpressureStrategy.BUFFER. In fact, it is.
FlowableOnSubscribe.subscribe(new BufferAsyncEmitter<T>(...))
Finally, we call BufferAsyncEmitter's onError method, which is equivalent to calling
public final void onError(Throwable e) { if (!tryOnError(e)) { RxJavaPlugins.onError(e); } } @Override public boolean tryOnError(Throwable e) { return error(e); } protected boolean error(Throwable e) { if (e == null) { e = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources."); } if (isCancelled()) { return false; } try { actual.onError(e); } finally { serial.dispose(); } return true; }
We see that when isCancelled (actually after dispose) return s false. Then you enter RxJavaPlugins.onError(e).
Note: RxJava Plugins is a Rxjava global config, which can configure a lot of quantities.
public static void onError(@NonNull Throwable error) { Consumer<? super Throwable> f = errorHandler; if (error == null) { error = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources."); } else { if (!isBug(error)) { error = new UndeliverableException(error); } } //Check whether global error handling is set. if (f != null) { try { f.accept(error); return; } catch (Throwable e) { // Exceptions.throwIfFatal(e); TODO decide e.printStackTrace(); // NOPMD uncaught(e); } } //If the uncaught method is not called directly, an error that is not caught is thrown. error.printStackTrace(); // NOPMD uncaught(error); }
At this point, we can see that when this happens, the onError method actually called throws errors that are not captured by rxjava directly.
Solve
There are two solutions. The first one is already available. We can actively configure errorHandler in RxJava Plugins for global capture. But that's not ideal.
The second way is not to call the onError method manually, so how to report an error? We can use onNext(null) to report errors. Look at the onNext method
@Override public void onNext(T t) { if (done || isCancelled()) { return; } if (t == null) { onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources.")); return; } queue.offer(t); drain(); }
When we have disposed, we do nothing, and actually do not call the onNext response registered in subscribe. That's exactly what we want, because the purpose of dispose is not to stop responding to the data sent?
If not dispose d, rxjava will throw an exception instead of calling the onError method, and we don't need to worry about it.
External transmission
If I simply change the example above
public static void test1(){ mDisposable = Flowable.create(new FlowableOnSubscribe<Object>() { @Override public void subscribe(FlowableEmitter<Object> emitter) throws Exception { //For example, make a network request Thread.sleep(5000); boolean b = false; // Simulate request status if(b){ emitter.onNext(new Object()); emitter.onComplete(); }else { emitter.onError(new RuntimeException("no data")); } } }, BackpressureStrategy.BUFFER).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Object>() { @Override public void accept(Object o) throws Exception { //TODO } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { throwable.printStackTrace(); } }); }
Thread.sleep no longer catches errors on its own. That would happen.
Let's analyze that when we call dispose, we actually call the interrpter method of the thread sending the data (see subscribeOn method for details), which uses sleep to interrupt and throw an exception.
try { source.subscribe(emitter); } catch (Throwable ex) { Exceptions.throwIfFatal(ex); emitter.onError(ex); }
When an exception is thrown by source.subscribe, the emitter.onError method is actually called to emit the error, which is back to what we said above.