Exceptions in Kotlin

Maxilect
7 min readFeb 21, 2020

--

Our company has been using Kotlin in production for more than two years. 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.

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

How can I handle errors in Kotlin?

There are several ways:

  • You can use some return value which indicates the fact that there is an error;
  • For the same purpose, you can use the indicator parameter,
  • Create a global variable,
  • Handle exceptions,
  • Add contracts (DbC).

Let’s consider each of the options in more detail.

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
Example 2

Indicator parameter

A certain parameter is passed to the function. After returning the value by the parameter, you can see whether there was an error inside the function.

Example

Global variable

Global variable works in approximately the same way.

Example

Exceptions

Exceptions are used almost everywhere.

Example

Contracts (DbC)

Frankly, I have never seen this approach in practice. By long “googling”, I found that Kotlin 1.3 has a library that allows the use of contracts. You can set conditions 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

Honestly, this library has terrible syntax. Perhaps that’s why I haven’t used it.

Exceptions in Java

Let’s move on to Java and how exceptions has worked initially.

Initially, two types of exceptions were laid:

  • checked;
  • unchecked.

What are “checked” exceptions made for? If a certain checked exception is possible, then it must be checked manually later. Theoretically, this approach should have led to the absence of unprocessed errors and improved code quality. But practice contradicts this idea. 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:

I am sure you have encountered quite a lot of code wrapped in try-catch, where “catch” is an empty block since the developer assumes that such a situation is impossible. In many cases, the handling of checked exceptions is implemented in the following way: they simply throw a RuntimeException and catch it somewhere else (or do not catch it …).

What is different in Kotlin?

For the exceptions, the Kotlin compiler is different:

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.

3. You can also use a similar construction when referring to some object that may be nullable:

Java compatibility

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

  • “Checked” exceptions from Java in Kotlin can’t be declared (since there are no checked exceptions in Kotlin).
  • “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). This annotation is made only for Java compatibility. But in practice, many people use it to declare a specific method can throw a specific kind of exception.

Alternative to the “try-catch”

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 connected methods. When business logic is spread over blocks or the entire call chain, it’s more difficult to understand how the application works. Also, this block harms readability.

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

We have the opportunity to use the “Try Monad”. Basically, this is a container that stores some value. “flatMap” is a method that deals with this container. It has current value and/or a function as input and then returns a monad.

In this case, the call is wrapped in the Try monad (we return Try). It can be processed in a specific 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

How can I use “Try”?

At 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 our projects, we used the self-made “Try” implementation — we managed that with one class and it did an excellent job.

Secondly, there is the “Arrow” library, which adds a lot of “functionality” to Kotlin. Surely, there are “Try” and “Either”.

Moreover, The Result class appeared in Kotlin 1.3, which I will discuss in more detail later.

“Try” with the “Arrow”

Arrow library gives us the “Try” class. It implements two states: Success or Failure:

  • Success will save our value if the output is successful,
  • Failure stores the exception that occurred during the execution of the code block.

This is how this class can be implemented:

The same class should implement the “flatMap” method, which allows us to pass a function and return our “Monad Try”:

What is this 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. We can have two situations: either we successfully received and combined them, or something failed. Therefore, we can do the following:

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

Here’s how it looks when we simulate that something fails:

We’ve used the same function with different input and got the RuntimeException.

Arrow library allows you to use constructs that are syntactic sugar, in particular — binding. All this can be done using “flatMap”, but binding allows you to make it readable:

Given that one of the results failed, we get an error on the output. You can use a similar monad 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:

Here is an example from practice. We have a request to the server, we process it, get the body from it and try to map it to our class.

In this case, we get “response.data” at the output, which we can process depending on the result. “Try-catch” would make this block sloppy.

Kotlin 1.3

In Kotlin 1.3, the Result class was introduced. 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.

This class is currently experimental. Language developers can change its

signature, behavior, or remove it all, 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. In fact, it can be used as a “Try”.

Conclusions

  • Functional error handling in Kotlin is simple and convenient;
  • Classical “try-catch” is legal (you can choose the most convenient error handling approach);
  • The absence of checked exceptions does not mean that errors can not be handled;
  • Uncaught exceptions on production lead to sad consequences.

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

--

--

Maxilect
Maxilect

Written by Maxilect

We are building IT-solutions for the Adtech and Fintech industries. Our clients are SMBs across the Globe (including USA, EU, Australia).

Responses (1)