Handling errors without using a try-catch block using the effective-kotlin way
I haven't read this book, but I imagine the author's point is that it's better to expose a sealed class result wrapper than an exception in your public functions, not that you should avoid using try-catch
on third-party or stdlib functions that can throw.
In your case, you have to use catch
to convert the exception into a result. I would say your code is fully implementing the author's advice. You can't help that the function you're calling is exposing a must-handle exception, but you can convert that into the better paradigm in your own public function's signature.
Kotlin did away with checked exceptions largely because of the problems they create. The whole call stack has to be exception-types aware in their signatures even if intermediate functions don't care about handling the errors, so this is very poor encapsulation and minor API changes can have a huge ripple effect. But removing checked exceptions creates the issue of a programmer possibly forgetting (or not knowing they need to) handle a possible error.
A sealed result wrapper class can alleviate both problems. Each function in the call stack can elect whether to pass the whole result back or intercept errors, and which types of errors to intercept. If they just pass it along, they don't need to know about the possible kinds of errors. None of the function signatures have to be modified if the error types are modified. And the programmer doesn't have anything they can forget to handle. Either they just pass the whole result along without looking at it, or they replace the error with a default value, or they can choose to actually respond to specific types of errors.
You can avoid the try-catch by using the Kotlin builtin Result class and code. (Behind the scenes, you have a try-catch - see source).
fun createFormattedText(status: String, description: String): ResultHandler<PricingModel> {
runCatching {
val product = description.format(status)
val listProducts = listOf(1, 2, 3)
ResultHandler.Success(PricingModel(product, listProducts))
}.getOrElse {
ResultHandler.Failure(it)
}
}
The topic of the chapter in the book is "Prefer null or Failure result when the lack of result is possible", so if you don't care about the exception, you can do this:
fun createFormattedText(status: String, description: String): PricingModel? {
runCatching {
val product = description.format(status)
val listProducts = listOf(1, 2, 3)
PricingModel(product, listProducts)
}.getOrNull()
}
For debugging/logging, that would also work:
fun createFormattedText(status: String, description: String): PricingModel? {
runCatching {
val product = description.format(status)
val listProducts = listOf(1, 2, 3)
PricingModel(product, listProducts)
}.onFailure {
log("Something wrong with $it")
}.getOrNull()
}
Unfortunately you can't replace your ResultHandler
with Kotlins Result
- because Result
can't be used as a return type. I found this post explaining the reasoning and also a workaround, hope it helps.
Alternatively you could build your own extension function for your ResultHandler
and move the handling of exceptions under the hood:
public inline fun <R> runCatching(block: () -> R): ResultHandler<R> {
return try {
ResultHandler.Success(block())
} catch (e: Throwable) {
ResultHandler.Failure(e)
}
}