Blog Infos
Author
Published
Topics
,
Author
Published

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:

class MyUseCase {
suspend operator fun invoke() = runCatching { delay(1000) }
}
scope.launch {
myUseCase()
.onSuccess { println("Success") }
.onFailure { println("Failure") }
println("We're not stopping!")
}
Thread.sleep(500) // Cancel while executing use case
scope.cancel()
// This will output the following:
// "Failure"
// "We're not stopping!"

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:

import kotlinx.coroutines.CancellationException
/**
* Like [runCatching], but with proper coroutines cancellation handling. Also only catches [Exception] instead of [Throwable].
*
* Cancellation exceptions need to be rethrown. See https://github.com/Kotlin/kotlinx.coroutines/issues/1814.
*/
inline fun <R> resultOf(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Like [runCatching], but with proper coroutines cancellation handling. Also only catches [Exception] instead of [Throwable].
*
* Cancellation exceptions need to be rethrown. See https://github.com/Kotlin/kotlinx.coroutines/issues/1814.
*/
inline fun <T, R> T.resultOf(block: T.() -> R): Result<R> {
return try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Like [mapCatching], but uses [resultOf] instead of [runCatching].
*/
inline fun <R, T> Result<T>.mapResult(transform: (value: T) -> R): Result<R> {
val successResult = getOrNull()
return when {
successResult != null -> resultOf { transform(successResult) }
else -> Result.failure(exceptionOrNull() ?: error("Unreachable state"))
}
}

Job Offers

Job Offers


    (Senior) Android Developer – Machine Learning (w/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Talent Acquisition Manager – Technology

    FanDuel
    New York, NY; Atlanta, GA
    • Full Time
    apply now

    Android Developer

    Yoti Ltd
    Anywhere
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

, ,

Automated migration of Android apps to Bazel build system

Migrating large projects that consist of hundreds or thousands of modules and being maintained by a large team, from Gradle to Bazel might be challenging. I would like to discuss the process of automation of…
Watch Video

Automated migration of Android apps to Bazel build system

Pavlo Stavytskyi
Software Engineer
Lyft

Automated migration of Android apps to Bazel build system

Pavlo Stavytskyi
Software Engineer
Lyft

Automated migration of Android apps to Bazel build system

Pavlo Stavytskyi
Software Engineer
Lyft

Jobs

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: @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.

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

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.

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

Menu