📜 ⬆️ ⬇️

Exceptions in Kotlin and their features

Our company has been using Kotlin in production for more than two years. Personally, I ran into this language about a year ago. There are many topics for conversation here, but today we'll talk about error handling, including in a functional style. I'll tell you how this can be done in Kotlin.

image

(Photos from a meeting on this topic, which took place in the office of one of the companies of Taganrog. Alexey Shafranov spoke - the leader of the working group (Java) in Maxillect)

How can you handle errors in principle?


I found several ways:
')

Let us dwell in more detail on each of the options.

Return value


Some “magic” value is returned if an error has occurred. If you have ever used scripting languages, you probably have seen similar constructs.

Example 1:

function sqrt(x) { if(x < 0) return -1; else return √x; } 

Example 2:

 function getUser(id) { result = db.getUserById(id) if (result) return result as User else return “Can't find user ” + id } 

Parameter indicator


Some parameter passed to the function is used. After returning the value of the parameter, you can see if there was an error inside the function.

Example:

 function divide(x,y,out Success) { if (y == 0) Success = false else Success = true return x/y } divide(10, 11, Success) id (!Success) //handle error 

Global variable


The global variable works in much the same way.

Example:

 global Success = true function divide(x,y) { if (y == 0) Success = false else return x/y } divide(10, 11, Success) id (!Success) //handle error 

Exceptions


We are all used to the exceptions. They are used almost everywhere.

Example:

 function divide(x,y) { if (y == 0) throw Exception() else return x/y } try{ divide(10, 0)} catch (e) {//handle exception} 

Contracts (DbC)


Frankly, I never saw this approach live. Through long googling, I found that in Kotlin 1.3 there is a library that actually allows you to use contracts. Those. You can put the condition on the variables that are passed to the function, the condition on the return value, the number of calls, where it is called from, etc. And if all the conditions are met, it is considered that the function worked correctly.

Example:

 function sqrt (x) pre-condition (x >= 0) post-condition (return >= 0) begin calculate sqrt from x end 

Frankly, this library has a terrible syntax. Perhaps that is why I did not see this live.

Exceptions in java


Let's turn to Java and to how it all worked initially.

image

When designing the language laid two types of exceptions:


What are checked exceptions for? Theoretically, they are needed so that people must check for errors. Those. if a defined exception is possible, then it must be checked later. Theoretically, such an approach should have led to the absence of unprocessed errors and an improvement in the quality of the code. But in practice it is not. I think everyone at least once in their life saw an empty catch block.

Why can it be bad?

Here is a classic example straight from the Kotlin documentation - the interface from the JDK, implemented in StringBuilder:

 Appendable append(CharSequence csq) throws IOException; try { log.append(message) } catch (IOException e) { //Must be safe } 

I'm sure you have seen quite a lot of code wrapped in try-catch, where catch is an empty block, since such a situation should not have happened, according to the developer. In many cases, checked exception handling is implemented in the following way: they simply throw a RuntimeException and catch it somewhere above (or not catch ...).

 try { // do something } catch (IOException e) { throw new RuntimeException(e); //  - ... 

What can be in Kotlin


In terms of exceptions, the Kotlin compiler differs in that:

1. Does not distinguish between checked and unchecked exceptions. All exceptions are only unchecked, and you yourself decide whether to catch and process them.

2. Try can be used as an expression - you can start a try block and either return the last line from it, or return the last line from a catch block.

 val value = try {Integer.parseInt(“lol”)} catch(e: NumberFormanException) { 4 } //  

3. It is also possible to use a similar construction when referring to an object that can be nullable:

 val s = obj.money ?: throw IllegalArgumentException(“ , ”) 

Java compatibility


Kotlin code can be used in Java and vice versa. How to handle exceptions?


Alternative to try-catch


The try-catch block has a significant drawback. When it appears, a part of the business logic is transferred inside the catch, and this can occur in one of the many methods above. When business logic is spread across blocks or the entire call chain, it’s more difficult to understand how an application works. And the readability blocks themselves are not added to the code.

 try { HttpService.SendNotification(endpointUrl); MarkNotificationAsSent(); } catch (e: UnableToConnectToServerException) { MarkNotificationAsNotSent(); } 

What are the alternatives?

One option offers us a functional approach to exception handling. A similar implementation looks like this:

 val result: Try<Result> = Try{HttpService.SendNotification(endpointUrl)} when(result) { is Success -> MarkNotificationAsSent() is Failure -> MarkNotificationAsNotSent() } 

We have the ability to use the monad Try. In essence, this is a container that stores some value. flatMap is a method of working with this container, which, together with the current value, can take a function and return the monad again.

In this case, the call is wrapped in a Try monad (we return a Try). This can be processed in the only place where we need it. If the output has a value, we perform subsequent actions with it; if we have thrown an exception, we process it at the very end of the chain.

Functional exception handling


Where can I get try?

First, there are quite a few implementations of the Try and Either classes from the community. You can take them or even write the implementation yourself. In one of the “combat” projects, we used the samopisny implementation of Try — we managed one class and did an excellent job.
Secondly, there is the Arrow library, which in principle adds a lot of functionality to Kotlin. Naturally, there is Try and Either.

Well, and besides, in Kotlin 1.3 a Result class appeared, which I will discuss in more detail later.

Try using the example of the Arrow library


The Arrow library gives us a Try class. In fact, it can be in two states: Success or Failure:


The call looks like this. Naturally, it is wrapped in the usual try - catch, but it will happen somewhere inside our code.

 sealed class Try<out A> { data class Success<out A>(val value: A) : Try<A>() data class Failure(val e: Throwable) : Try<Nothing>() companion object { operator fun <A> invoke(body: () -> A): Try<A> { return try { Success(body()) } catch (e: Exception) { Failure(e) } } } 

The same class should implement the flatMap method, which allows you to pass a function and return our monad try:

 inline fun <B> map(f: (A) -> B): Try<B> = flatMap { Success(f(it)) } inline fun <B> flatMap(f: (A) -> TryOf<B>): Try<B> = when (this) { is Failure -> this is Success -> f(value) } 

What is it for? In order not to handle errors for each of the results, when we have several of them. For example, we received several values ​​from different services and want to combine them. In fact, we can have two situations: either we received them successfully and combined them, or something fell. Therefore, we can proceed as follows:

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Success(value=15) 

If both calls succeed and we get the values, we execute the function. If they are not successful, then Failure will return with an exception.

Here’s what it looks like if something fell:

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { throw RuntimeException(“Oh no!”) } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no! 

We used the same function, but the output is a Failure from RuntimeException.

Also, the Arrow library allows you to use constructs that are in fact syntactic sugar, in particular binding. All the same can be rewritten via a serial flatMap, but the binding makes it readable.

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val result3: Try<Int> = Try { throw RuntimeException(“Oh no, again!”) } val sum = binding { val (one) = result1 val (two) = result2 val (three) = result3 one + two + three } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no, again! 

Given that one of the results has fallen, we get an error at the output.

Such a monad can be used for asynchronous calls. Here, for example, two functions that run asynchronously. We also combine their results in the same way, without checking their states separately:

 fun funA(): Try<Int> { return Try { 1 } } fun funB(): Try<Int> { Thread.sleep(3000L) return Try { 2 } } val a = GlobalScope.async { funA() } val b = GlobalScope.async { funB() } val sum = runBlocking { a.await().flatMap { one -> b.await().map {two -> one + two } } } 

But a more “combat” example. We have a request to the server, we process it, get the body out of it and try to map it to our class, from which we are already returning data.

 fun makeRequest(request: Request): Try<List<ResponseData>> = Try { httpClient.newCall(request).execute() } .map { it.body() } .flatMap { Try { ObjectMapper().readValue(it, ParsedResponse::class.java) } } .map { it.data } fun main(args : Array<String>) { val response = makeRequest(RequestBody(args)) when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } } 

Try-catch would make this block much less readable. And in this case, we get response.data at the output, which we can process depending on the result.

Result from Kotlin 1.3


In Kotlin 1.3 introduced the class Result. In fact, it is something like Try, but with a number of restrictions. It is initially supposed to be used for various asynchronous operations.

 val result: Result<VeryImportantData> = Result.runCatching { makeRequest() } .mapCatching { parseResponse(it) } .mapCatching { prepareData(it) } result.fold{ { data -> println(“We have $data”) }, exception -> println(“There is no any data, but it's your exception $exception”) } ) 

If I'm not mistaken, this class is currently experimental. The developers of the language can change its signature, behavior, or even remove it, so at the moment it is forbidden to use it as a return value from a method or variable. However, it can be used as a local (private) variable. Those. In fact, it can be used as a try from the example.

findings


Conclusions that I made for myself:


Article author: Alexey Shafranov, leader of the working group (Java), Maxilect company

PS We publish our articles on several sites Runet. Subscribe to our pages on the VK , FB or Telegram channel to find out about all our publications and other news from Maxilect.

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


All Articles