📜 ⬆️ ⬇️

Functional error handling in Kotlin using Arrow

image

Hi, Habr!

Everyone loves runtime exceptions. There is no better way to know that something was not taken into account when writing code. Especially - if exceptions crash the application from millions of users, and this news comes in a panicky email from the analytics portal. Saturday morning. When you are on a country trip.
')
After this, you seriously think about error handling - and what opportunities does Kotlin offer us?

The first try-catch comes to mind. For me it's a great option, but it has two problems:

  1. This is, after all, an extra code (a forced wrapper around the code, which does not have the best effect on readability).
  2. Not always (especially when using third-party libraries) from the catch block it is possible to receive an informative message that specifically caused the error.

Let's see what try-catch turns the code into when trying to solve the above problems.

For example, the simplest function to perform a network request

fun makeRequest(request: RequestBody): List<ResponseData>? { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } 

becomes like

 fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null } } 

“It's not so bad,” someone might say, “you want all the sugar code with your cotlin,” he adds (this is a quote) - and it will be ... right twice. No, there will be no holivars today - everyone decides for himself. I personally ruled the code of a self-written json parser, where the parsing of each field was wrapped in try-catch, with each of the catch blocks being empty. If someone is satisfied with this state of things - the flag in hand. I want to suggest a better way.

In most typed functional programming languages, two classes are offered for handling errors and exceptions: Try and Either . Try to handle exceptions, and Either to handle business logic errors.

The Arrow library allows you to use these abstractions with Kotlin. Thus, you can rewrite the above query as follows:

 fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() } } 

How is this approach different from using try-catch?

First of all, anyone who will read this code after you (and there are likely to be such) will be able to understand by the signature that the execution of the code can lead to an error - and write the code for its processing. Moreover, the compiler will scream if it is not done.

Secondly, there is flexibility in how the error can be handled.

Inside Try, the error or success of the execution is represented as the Failure and Success classes, respectively. If we want the function to always return something on error, we can set the default value:

 makeRequest(request).getOrElse { emptyList() } 

If error handling is more complicated, fold comes to the rescue:

 makeRequest(request).fold( {ex -> //  -       emptyList() }, { data -> /*    */ } ) 

You can use the recover function - its contents will be completely ignored if Try returns Success.

 makeRequest(request).recover { emptyList() } 

You can use for comprehensions (borrowed by the Arrow creators from Scala) if you need to handle the result of a Success using a sequence of commands, by calling the factory .monad () on Try:

 Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result) } 

The variant above can be written without using binding, but then it will be read differently:

 httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) } 

In the end, the result of the function can be processed with when:

 when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } 

Thus, using Arrow, you can replace the far from ideal try-catch with something flexible and very convenient. The additional advantage of using Arrow is that despite the fact that the library positions itself as functional, you can use separate abstractions from there (for example, the same Try) by continuing to write good old OOP code. But I’ll warn you, you may like it and you’ll be drawn into it, in a couple of weeks you will start to learn Haskell, and your colleagues will very soon stop understanding your reasoning about the code structure.

PS: It's worth it :)

Source: https://habr.com/ru/post/435254/


All Articles