When it comes to RxJava2 error handling, the first thing that comes to mind is the OnErrorNotImplementedException and implementing onError to handle this.

To err is git commit; to fix is git revert.

While onError seems a viable choice, IMO, it is better to take control of errors and leave onError to highlight only unforeseen errors.

In this article, I’ll be sharing a generic technique using onErrorReturn and some cool Kotlin features like sealed classes, smart casts and extension functions for RxJava error handling in a Clean architecture setup.

1. Example Scenario

Let’s imagine an app where a user wants to ‘like’ a comment on a photo. For this we need the signed in user’s userId . Once we have that, we can use it to make an API call to ‘like’ the comment.

2. How the calling code (client) should look like

The client should be at the right level of abstraction. In our example, it should only worry about ‘liking’ a comment and not deal with user retrieval.

Note in the code snippet below, the client simply invokes a UseCase and only handles its results and errors.

A view model for a Comment that has a ‘like’ button.

Key takeaways

(1) Client only needs to provide comment.id . LikeCommentUseCase internally handles fetching of other required information.

(2) & (3) Every UseCase execution returns a Result<T> that the client can use to ‘handle results’.

(4) On error, the client can check for the right instance and handle the error accordingly. How does the client know what errors to handle, you ask? Every UseCase should document the errors it can throw (as shown below). Such documentation can be automated through code generation.

3. How a UseCase composes other UseCase(s).

A UseCase can compose other UseCases and repositories. Complex UseCases are built on top of simpler ones.

A UseCase implementing the logic to like a comment using the logged in user’s userId

Key takeaways

(1) Gets the signed in user’s cached userId and checks with the server if the user is still valid. This can result in a UserNotFoundError to indicate that the user is not longer active.

(2) & (4) Forces us to handle errors from upstream. The emitted object is of type Result . A Result can either be a Result.OnSuccess or a Result.OnError . We can handle the error here or use Result<T>.toData() for automatic handling/conversion from a Result<T> to T .

(3) & (5) Updates the comment and fires analytics events. This can result in a CommentNotFoundError .

(6) In case of any error, converts a Throwable back to Result<T>.

Completing the picture

The code snippets above explain how this approach keeps the code readable. But to get a better understanding of how it actually works, here are the helper classes and methods that the code above uses.

Result and Error classes

Hope you found this approach interesting. Let me know your thoughts on this. And as always, thank you for reading. :)

Fin.