Article
Tidy up your Observable Streams with Kotlin’s Sealed Classes
August 30, 2017

If you have ever read a sterile how-to example of using RxJava in your network layer, you might find this familiar:
userService.updateEmail("[email protected]")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<UserInfo>() {
//...
})
Here we have some asynchronous call to update an email address, and an Observer
standing ready to handle the result. As samples go, it’s not trying to solve more problems than absolutely necessary while demonstrating the basics.
But just beyond the basics are the every-day error conditions:
- Permission Denied
- Sign In Required
- Invalid Email

You might think to handle your error states inside onError(Throwable t)
, but there are good reasons not to . onError()
should be reserved for when you’re unlikely to gracefully recover. However, we can gracefully handle errors like permission rejection or invalid inputs. Rather than violently terminating the stream with an Exception
, these error conditions should be pushed through onSuccess().
But how can we model the success case and all the expected error cases in one stream of UserInfo
? Kotlin has a beautiful solution, but before looking at that, lets examine a Java approach and observe its problems.
The Java Way
Suppose we combine both successes and expected errors into a composite object, such that you can inspect the payload for success or failure in a single model:
onSuccess(Either<UserInfo, EmailUpdateError> result)
Here an Either
holds two Optional<T>
fields and allows you check which is present.
In fact, Retrofit’s Response (and Result ) wrappers are built with a similar idea. If you opt-in, Retrofit’s RxJava Call Adapter will wrap most non-200s in Result<T>
and send them through onSuccess()
.
But Java makes handling compound Nullable
types very ugly. Response<T>
does a fine job guarding against nullability, but any composite object like Response<T>
or Either<Success, Failure>
relies on the consuming developer to manually access Nullable
(or Optional<T>
) fields:
if (either.isSuccess()) {
UserInfo userInfo = either.success().get();
// handle success
} else {
EmailUpdateFailure failure = either.failure().get();
// handle failure
}
This increases branching, depends on documentation contracts, and is verbose. Any model type of your own creation will have the same core problem.
And don’t even think about using .zipWith()
to coalesce multiples: