Kotlin coroutines error handling strategy — runCatching
and Result
class
I am trying to learn Kotlin coroutines, and was trying to learn more about how to handle errors from suspended functions. One of the recommended way by Google is to create a “Result” class like the following:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
This allows us to take advantage of Kotlin’s when like following:
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
However, I have recently stumbled into Kotlin’s runCathing {} API that makes use of Result class available in standard lib since 1.3
Here I will try to explore how the native API can replace the recommended example in the Android Kotlin training guide for simple use cases.
Based on Kotlin standard lib doc, you can use runCatching { } in 2 different ways. I will focus on one of them, since the concept for other one is similar.
To handle a function that may throw an exception in coroutines or regular function use this:
val statusResult: Result<String> = runCatching {
// function that may throw exception that needs to be handled
repository.userStatusNetworkRequest(username)
}.onSuccess { status: String ->
println("User status is: $status")
}.onFailure { error: Throwable ->
println("Go network error: ${error.message}")
}
// Assuming following supposed* long running network API
suspend fun userStatusNetworkRequest(username: String) = "ACTIVE"
Notice the ‘Result’ returned from the runCatching this is where the power comes in to write semantic code to handle errors.
The onSuccess and onFailrue callback is part of Result class that allows you to easily handle both cases.
How to handle Exceptions
In addition to nice callbacks, the Result class provides multiple ways to recover from the error and provide a default value or fallback options.
-
Using
getOrDefault()
andgetOrNull()
API
val status: String = statusResult.getOrDefault("STATUS_UNKNOWN")
// Or if nullable data is acceptable use:
val status: String? = statusResult.getOrNull()
Since the onSuccess and onFailure returns Result you can chain most of these API calls like following
val status: String = runCatching {
repository.userStatusNetworkRequest("username")
}
.onSuccess {}
.onFailure {}
.getOrDefault("STATUS_UNKNOWN")
2. Using recover { }
API
The recover API allows you to handle the error and recover from there with a fallback value of the same data type. See the following example.
val status: Result<String> = runCatching {
repository.userStatusNetworkRequest("username")
}
.onSuccess {}
.onFailure {}
.recover { error: Throwable -> "STATUS_UNKNOWN" }
println(status.isSuccess) // Prints "true" when error is received
3. Using fold {}
API to map data
The fold extension function allows you to map the error to a different data type you wish. In this example, I kept the user status as String.
val status: String = runCatching {
repository.userStatusNetworkRequest("username")
}
.onSuccess {}
.onFailure {}
.fold(
onSuccess = { status: String -> status },
onFailure = { error: Throwable -> "STATUS_UNKNOWN" }
)
Aside from these, there are some additional useful functions and extension functions for Result , take a look at official documentation for more APIs.
I hope this was useful or a new discovery for you as it was for me 😊
Top comments (0)