Blog Infos
Author
Published
Topics
Author
Published
Topics
Simplifying error handling using Kotlin’s sealed interfaces.

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:

// Old: plain try-catch

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:

// Better: using Result

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:

// Old: Result with Exceptions

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:

  1. 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. ⚠️
  2. Hidden errors: The exceptions from the network layer are hidden. Maybe it throws IOExceptions or something custom like a RequestException? Maybe upload() uses GraphQL and we get ApolloIOExceptions? ⚠️
  3. 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:

// Improved: Result with typed domain errors

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
    }
}

OUR VIDEO RECOMMENDATION

Jobs

No results found.

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:

// Improved way of acting upon errors

createPostUseCase.execute()
    .onSuccess {
        // Hooray!
        showSuccess()
    }
    .onFailure { error ->
        when (error) {
            is ValidationFailed -> showValidationError(error)
            NetworkIssue -> showUploadFailed()
        }
    }

This fixes the limitations we identified above:

  1. Discoverability: ✅ Fixed! In onFailure we get a typed Error. We can click through on it in the IDE to see all possible error scenarios.
  2. Hidden errors: ✅ Fixed! Network errors are no longer hidden, because Error is sealed and cannot be expanded outside of the use case.
  3. 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.

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu