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 nativeResult<T>
class already available in standard lib since Kotlin v1.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<T>
class that allows you to easily handle both cases.
How to handle Exceptions
In addition to nice callbacks, the Result<T>
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<T>
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" even if error is thrown
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<T>
, 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 😊
UPDATE #1: As Gabor has mentioned below, there is an unintended consequence about using it in coroutines. I will look into it and provide more updates on the usage soon. Thanks to Garbor for mentioning it.