Kotlin exceptions and their features

    Our company has been using Kotlin in production for more than two years. Personally, I came across this language about a year ago. There are many topics to talk about, but today we’ll talk about error handling, including in a functional style. I'll tell you how to do this in Kotlin.

    image

    (Photo from the meeting on this topic, which took place in the office of one of the Taganrog companies. Alexey Shafranov, the leader of the working group (Java) at Maxilekt, spoke)

    How can you handle errors in principle?


    I found several ways:

    • you can use some return value as a pointer to the fact that there 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


    A certain “magic” value is returned if an error occurs. If you've ever used scripting languages, you must 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
    }
    

    Indicator parameter


    A certain parameter passed to the function is used. After returning the value by the parameter, you can see whether 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 approximately 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 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 have never seen this approach live. By long googling, I found that Kotlin 1.3 has a library that actually allows the use of contracts. Those. you can set condition on variables that are passed to the function, condition on the return value, the number of calls, where it is called from, etc. And if all the conditions are met, it is believed that the function worked correctly.

    Example:

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

    Honestly, this library has terrible syntax. Perhaps that’s why I haven’t seen such a thing live.

    Exceptions in Java


    Let's move on to Java and how it all worked from the start.

    image

    When designing a language, two types of exceptions were laid:

    • checked - checked;
    • unchecked - unchecked.

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

    Why can this be bad?

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

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

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

    try {
    	// do something
    }
    catch (IOException e) {
    	throw new RuntimeException(e); // там где-нибудь поймаю...
    

    What is possible in Kotlin


    From the point of view of exceptions, the Kotlin compiler is different in that:

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

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

    val value = try {Integer.parseInt(“lol”)}
    	catch(e: NumberFormanException) { 4 } //Рандомное число
    

    3. You can also use a similar construction when referring to some object, which may 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 be neither checked nor declared (since there are no checked exceptions in Kotlin).
    • Possible checked exceptions from Kotlin (for example, those that came originally from Java) are not required to be checked in Java.
    • If it is necessary to check, the exception can be made verifiable using the @Throws annotation in the method (it is necessary to indicate which exceptions this method can throw). The above annotation is only for Java compatibility. But in practice, many people use it to declare that such a method, in principle, can throw some kind of exception.

    Alternative to try-catch block


    The try-catch block has a significant drawback. When it appears, part of the business logic is transferred inside the catch, and this can happen in one of the many methods above. When business logic is spread out over blocks or the entire call chain, it’s more difficult to understand how the application works. And the readability blocks themselves do not add 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 =
    Try{HttpService.SendNotification(endpointUrl)}
    when(result) {
    	is Success -> MarkNotificationAsSent()
    	is Failure    -> MarkNotificationAsNotSent()
    }
    

    We have the opportunity to use the Try monad. 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, again, return a monad.

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

    Functional Exception Handling


    Where can I get Try?

    First, there are quite a few community implementations of the Try and Either classes. You can take them or even write an implementation yourself. In one of the “combat” projects, we used the self-made Try implementation - we managed with 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 are Try and Either.

    Well, in addition, the Result class appeared in Kotlin 1.3, which I will discuss in more detail later.

    Try using the Arrow library as an example


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

    • Success on successful withdrawal will retain our value,
    • Failure stores an exception that occurred during the execution of a block of code.

    The call is as follows. Naturally, it is wrapped in a regular try - catch, but this will happen somewhere inside our code.

    sealed class Try {
    	data class Success(val value: A) : Try()
    	data class Failure(val e: Throwable) : Try()
    	companion object {
    		operator fun  invoke(body: () -> A): Try {
    		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 try monad:

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

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

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

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

    Here's what it looks like if something fell:

    val result1: Try = Try { 11 }
    val result2: Try = 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 a 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 through a serial flatMap, but binding allows you to make it readable.

    val result1: Try = Try { 11 }
    val result2: Try = Try { 4 }
    val result3: Try = 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 on the output.

    A similar monad can be used for asynchronous calls. For example, here are two functions that run asynchronously. We combine their results in the same way, without separately checking their status:

    fun funA(): Try {
    	return Try { 1 }
    }
    fun funB(): Try {
    	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 }
    	}
    }
    

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

    fun makeRequest(request: Request): Try> =
    	Try { httpClient.newCall(request).execute() }
    		.map { it.body() }
    		.flatMap { Try { ObjectMapper().readValue(it, ParsedResponse::class.java) } }
    		.map { it.data }
    fun main(args : Array) {
    	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 of Kotlin 1.3


    Kotlin 1.3 introduced the Result class. In fact, it is something similar to Try, but with a number of limitations. It is originally intended to be used for various asynchronous operations.

    val result: Result = 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 not mistaken, this class is currently experimental. Language developers can change its signature, behavior, or remove it altogether, so at the moment it is forbidden to use it as a return value from methods or a variable. However, it can be used as a local (private) variable. Those. in fact, it can be used as a try from the example.

    conclusions


    Conclusions that I made for myself:

    • functional error handling in Kotlin is simple and convenient;
    • no one bothers to process them through try-catch in the classical style (both that and that has the right to life; both that and that are convenient);
    • the absence of checked exceptions does not mean that errors can not be handled;
    • uncaught exceptions on production lead to sad consequences.

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

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

    Also popular now: