Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling errors without using a try-catch block using the effective-kotlin way

Kotlin 1.3.61

I have been reading the Effective Kotlin by Marcin Moskala book. And found the item on handling errors interesting as it discourages using a try-catch block and instead custom handler class

Quote from the book:

Using such error handling is not only more efficient than the try-catch block but often also easier to use and more explicit

However, there are cases where a try-catch cannot be avoided. I have the following snippet

class Branding {

        fun createFormattedText(status: String, description: String): ResultHandler<PricingModel> {
            return try {
                val product = description.format(status)
                val listProducts = listOf(1, 2, 3)

                ResultHandler.Success(PricingModel(product, listProducts))
            }
            catch (exception: IllegalFormatException) {
                ResultHandler.Failure(Throwable(exception))
            }
        }
    }

    class PricingModel(val name: String, products: List<Int>)

So the description.format(status) will throw an exception if it fails to format

This is my HandlerResult class, and what the book recommends:

sealed class ResultHandler<out T> {
    class Success<out T>(val result: T) : ResultHandler<T>()
    class Failure(val throwable: Throwable) : ResultHandler<Nothing>()
}

class FormatParsingException: Exception()

And how I use them in my code:

fun main() {
    val branding = Branding()
    val brand = branding.createFormattedText("status", "$%/4ed")

    when(brand) {
        is Success -> println(brand.result.name)
        is Failure -> println(brand.throwable.message)
    }
}

My question is. Is this one of those cases where a try-catch cannot be avoided. Or could I still return a Failure if the format was to fail while not use the try-catch?

like image 281
ant2009 Avatar asked Mar 03 '20 16:03

ant2009


People also ask

Can I use if else instead of try catch?

Try/catch is used when an Exception can be thrown from the code, and you catch it in the catch-clause, which is an object oriented way to handle errors. You can't catch Exceptions with if/else blocks - they share nothing with try/catch.

How do you handle exceptions in Kotlin?

In Kotlin, we use try-catch block for exception handling in the program. The try block encloses the code which is responsible for throwing an exception and the catch block is used for handling the exception. This block must be written within the main or other methods.

What is the purpose of a try block in the Kotlin programming language?

Kotlin try-catch block is used for exception handling in the code. The try block encloses the code which may throw an exception and the catch block is used to handle the exception. This block must be written within the method. Kotlin try block must be followed by either catch block or finally block or both.

Can we have catch () without try?

We can't have catch or finally clause without a try statement. We can have multiple catch blocks with a single try statement. try-catch blocks can be nested similar to if-else statements. We can have only one finally block with a try-catch statement.


Video Answer


2 Answers

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)
    }
}
like image 198
CFrei Avatar answered Nov 17 '22 04:11

CFrei


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.

like image 34
Tenfour04 Avatar answered Nov 17 '22 04:11

Tenfour04