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.
(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:
')
- You can use some return value as a pointer to what is an error;
- you can use the indicator parameter for the same purpose,
- enter a global variable
- handle exceptions
- add contracts (DbC) .
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.

When designing the language laid two types of exceptions:
- checked - checked;
- unchecked - unchecked.
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?
- Checked exceptions from Java in Kotlin can not be checked and not declared (since there are no checked exceptions in Kotlin).
- Possible checked exceptions from Kotlin (for example, appearing originally from Java) are not necessary to check in Java.
- If verification is necessary, an exception can be made verifiable using the @Throws annotation in the method (you must specify which exceptions this method can throw). This annotation is only for compatibility with Java. But in practice many of us use it to declare that in principle such a method can convey some 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:
- Success with successful output will keep our value,
- Failure stores an exception that occurred during the execution of a block of code.
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:
- functional error handling in Kotlin is easy and convenient;
- no one bothers to handle them through try-catch in a classic style (and this and that has the right to life, and this and that is convenient);
- the absence of checked exceptions does not mean that you can not handle errors;
- not caught exceptions on production lead to sad consequences.
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.