.doOnError() is a bit of a different beast. It is used to intercept errors that haven't reached .subscribe() yet. You may wonder how this is possible; is because the flow can be intercepted and recovered from the exception. In such a case, the original exception will never reach the error handler if it is implemented in the .subscribe() section.
For example, we can build on a previous case with .onExceptionResumeNext():
Observable.<String>error(new RuntimeException("Crash!"))
.doOnError(e -> log("doOnNext", e))
.onExceptionResumeNext(Observable.just("Second"))
.subscribe(item -> {
log("subscribe", item);
}, e -> log("subscribe", e));
Here, we've added a line:
.doOnError(e -> log("doOnNext", e))
It will intercept the exception before it reaches the .onExceptionResumeNext() block. So the output, in this case, will be as shown:
doOnNext:main: error
java.lang.RuntimeException: Crash!
subscribe:main:Second
Again, it's a super useful tool when there is a need to display notifications in the Android UI or just log that something failed in general.