Skip to content

Resilient use cases with kotlin.Result, coroutines and annotations

When writing use case classes, an important thing to consider is the output for failures. For example, we would probably expect a PostCommentUseCase to throw some sort of 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 the required error handling in calling locations. The Result class can solve this issue and help make your code more resilient, but there are some pitfalls to be aware of.

Try-catch

First, let us see what problem Result solves. Writing plain try-catches around every use case you call works fine, but has some disadvantages:

  1. No obligation to catch exceptions, easy to forget. ❌
  2. Inconsistent API if some use cases throw exceptions while others don’t require any try-catch. ❌
  3. Difficult to find where try-catch is missing. Airplane mode could prevent code that actually throws exceptions from ever being reached. ❌

Result class and its extensions

The Result class is a simple data structure to wrap data using Result.success(myData) or an exception using Result.failure(ex). There are useful methods to handle either of these values. With the runCatching extension you do not have to manually instantiate the Result class:

class PostCommentUseCase() {
    fun execute(comment: Comment) = runCatching {
        // If any of this code throws an exception,
        // this block wrap it in a Result.
        validate(comment)

        // Else, the return value of upload() will be wrapped in a Result.
        upload(comment)
    }
    
    // validate() & upload() methods left out
}

This can now be easily executed:

PostCommentUseCase().execute()
    .onSuccess {
        // Success!
        showCommentPosted()
    }
    .onFailure { e->
        // Log the exception and inform the user.
        showError(e)
    }

This is a very clear API and it fixes the issues listed above:

  1. No obligation to catch: ✅ Fixed! You either write .onSuccess { } or .onFailure { }. Or both, or none!
  2. Inconsistent API: ✅ Fixed! Every use case returns the Result wrapper. No more surprises! If you use Flow, you could make use cases return either Result or Flow directly. That way, you have only two solid return types to deal with.
  3. Difficult to locate missing try-catches: ✅ Fixed! You can focus on writing success/error handling when needed only without having to worry about exceptions popping up unexpectedly.

If you simply want to use the return value inside an existing try-catch or don’t care about if it fails, you can take a look at getOrThrow() and getOrNull().

Downsides

While using Result with this runCatching method is very simple, there are some flaws:

  1. runCatching catches Throwable. This also means it catches Errors like OutOfMemoryError. See StackOverflow – Difference between using Throwable and Exception in a try catch for why this should not be always be done. I share a proposed solution for this further down.
  2. runCatching does not rethrow cancellation exceptions. This makes it a bad choice for coroutines as it breaks structured concurrency. Read more about this including a proposed solution further down.
  3. It is not possible to specify a non-Exception error type or a custom base exception class. Alternatives like kotlin-result do offer this functionality.

The issue with suspend runCatching

The example below shows a use case that simply waits 1000 ms before returning inside runCatching. It launches it, then waits 500 ms and cancels the scope — while the use case is running. You would expect the entire coroutine block to stop executing, but it does not:

This is because the CancellationException being thrown while the use case is executing, is swallowed by runCatching and is thus never propagated to the outer scope. The result is that your code continues running even when, for example, a ViewModel’s scope is cancelled. It could lead to bugs and/or crashes.

Proposal: resultOf

While the Kotlin team is figuring out what to do with this issue (https://github.com/Kotlin/kotlinx.coroutines/issues/1814), we can write a variant of the runCatching (and mapCatching) method that fixes these issues:

It works mostly the same, is equally simple to use, but fixes the issues mentioned above:

  1. Catching Throwable: ✅ Fixed! This one catches Exception instead, not swallowing runtime errors.
  2. Catching cancellation exceptions: ✅ Fixed! It passes through the CancellationException, making coroutines behave as expected.

Updating the example from above is easy:

class PostCommentUseCase() {
    fun execute(comment: Comment) = resultOf { // Only this line changed.
        // The rest of the code is the same as above.
    }
}

Bonus for Android: @CheckResult annotation

While I listed not leaking exceptions as a plus, it can result in kicking off a use case and forgetting to do something with the returned Result value. For this reason, I always add @CheckResult above my use case invocation methods. Now the IDE will make sure to remind me to do at least something with the returned value of your use case. Note that this is only available in the AndroidX libraries.

This will make our final snippet look like this:

class PostCommentUseCase() {

    @CheckResult
    fun execute(comment: Comment) = resultOf {
        // If any of this code throws an exception,
        // this block wrap it in a Result.
        validate(comment)

        // Else, the return value of upload() will be wrapped in a Result.
        upload(comment)
    }
}

Further reading