You can reply to this article at https://proandroiddev.com/resilient-use-cases-less-exceptions-more-results-732eba4d0180.
One and a half years ago I wrote a post suggesting the use of kotlin.Result
instead of plain try-catches: Resilient use cases with kotlin.Result, coroutines and annotations. This article is a follow-up to that one, with an updated approach.

Quick recap: the issue
Consider a CreatePostUseCase
that throws a network exception when it fails. Since Kotlin does not have checked exceptions (i.e. the compiler does not enforce you to catch anything), it is easy to forget to add error handling in the calling locations. Using the built-in Result class can solve this issue and help make your code more resilient, as long as you’re aware of coroutine cancellations (see original post).
By making use cases return a kotlin.Result
, calling them changes from this:
try {
createPostUseCase.execute()
} catch(e: NetworkException) {
showError(e)
}
to something that’s not only easier to read but also contains any exception that’s thrown, so your app doesn’t crash if that’s forgotten:
createPostUseCase.execute()
.onSuccess {
// Hooray!
showSuccess()
}
.onFailure { e ->
// Log the exception and inform the user.
showError(e)
}
However, this still deals with a generic Exception
that’s being passed in the failure block. You’re not informed what it may be (unless through documentation) and the compiler doesn’t help in dealing with all possible types of errors. Let’s take the approach further and improve on that!
Original code: Result with Exceptions
Using the original approach, a use case that does a bit of validation and then uploads would look like this:
class CreatePostUseCase {
@CheckResult
fun execute(post: Post) = resultOf {
// First validate
when {
post.title.isEmpty() -> throw EmptyTitleException()
post.content.isEmpty() -> throw EmptyContentException()
}
// Then upload, which might throw exceptions from the network layer
upload(post)
}
class EmptyTitleException : Exception()
class EmptyContentException : Exception()
}
It works well for preventing crashes (because exceptions are always caught and wrapped into a Result
), but not in reducing functional errors over time, because of these limitations:
- Discoverability: We see the two exceptions for empty title/content only when we look at the code. In a calling location we are not informed about them, so we could forget handling them in places where it might be important. ⚠️
- Hidden errors: The exceptions from the network layer are hidden. Maybe it throws
IOExceptions
or something custom like aRequestException
? Maybeupload()
uses GraphQL and we getApolloIOExceptions
? ⚠️ - Adding new errors: Adding new exceptions doesn’t force handling them in calling locations of this use case. Imagine adding a new validation and your UI telling “Upload failed” instead of “Title is too long“. ⚠️
Introducing Kotlin-Result
This library, available at https://github.com/michaelbull/kotlin-result/, can be seen as a replacement for the built-in Result
class. The major difference is that in the failure branch, it doesn’t give you an Exception
, but an error type you specify yourself. By specifying this domain error type in a sealed Kotlin structure, we can fix the abovementioned limitations.
Let’s update the example use case:
class CreatePostUseCase {
@CheckResult
fun execute(post: Post) {
// First validate
when {
post.title.isEmpty() -> return ValidationFailed(emptyTitle = true)
post.content.isEmpty() -> return ValidationFailed(emptyContent = true)
}
// Then upload, while mapping network errors to our own type
return runSuspendCatching { upload(post) }.mapError { NetworkIssue }
}
sealed interface Error {
class ValidationFailed(
val emptyTitle: Boolean,
val emptyContent: Boolean
) : Error
object NetworkIssue : Error
}
}
Note that we’re using runSuspendCatching
. This extension is available in kotlin-result-coroutines
by default and supports coroutine cancellations (like my resultOf
extension I shared in the post previous year).
Now, when updating the calling code we can use when
to let the compiler help us handle all errors:
createPostUseCase.execute()
.onSuccess {
// Hooray!
showSuccess()
}
.onFailure { error ->
when (error) {
is ValidationFailed -> showValidationError(error)
NetworkIssue -> showUploadFailed()
}
}
This fixes the limitations we identified above:
- Discoverability: ✅ Fixed! In
onFailure
we get a typedError
. We can click through on it in the IDE to see all possible error scenarios. - Hidden errors: ✅ Fixed! Network errors are no longer hidden, because
Error
issealed
and cannot be expanded outside of the use case. - Adding new errors: ✅ Fixed! Since we’re using a
sealed interface
when we add another type in the use case, the compiler will notify us that calling locations need to be updated.
Summary
The original approach works well for preventing crashes, but by using typed domain errors we can make the Result pattern even more powerful. There’s more stuff in the library, like mapping, binding, transforming et cetera. See https://github.com/michaelbull/kotlin-result/ for all capabilities.
Alternatively, the Arrow library also offers a typed Either implementation, which may be interesting if you (would like to) use more things from that library. Personally, I prefer kotlin-result
because it’s focused on one thing and uses successful/failure
naming instead of Arrow’s left/right
, but in the end both libraries unlock better error handling.