Kotlin and Exceptions

Roman Elizarov
8 min readJun 10, 2020

--

Photo by John Mark Arnold on Unsplash

What are Kotlin Exceptions and how should you use them? To figure it out let’s look at their origins first. Exceptions came to Kotlin from Java. The story with exceptions in Java is complicated, though. I’ll give a brief overview.

The Origin

Java has a unique concept of checked exceptions that were designed to solve the problem of verbose and error-prone error-handling (pun intended). In languages predating Java, like in venerable C, you have to write code like shown in this snippet when doing basic input/output:

file = fopen("file.txt", "r");
if (file == NULL) {
// handle error & return
}
// work with file, check for error after each file operation

Every time you perform an operation that might fail due to some external circumstance, which happens especially often with files and network, you have to write code that checks the corresponding error condition and handles it. That’s tedious, easy to forget, hard to debug.

Java set on a noble goal to eliminate this problem. The solution was to use checked exceptions. Every file I/O operation is declared as throws IOException in Java and the compiler checks that you either handle it or declare that you rethrow it. The beauty of this is that you can write exception-handling code once for a whole bunch of I/O operations and you cannot forget writing it since the Java compiler is there to help you.

file = FileInputStream("file.txt"); // throws IOException

No error-handling boilerplate, no more missed error checks. It was such a bliss to program in Java… for a while.

Problems

Problems with checked exceptions accumulated over the years. Memory input/output APIs like ByteArrayInputStream were still declared to throw IOException that you had to handle even though it never happened, people abused checked exceptions in API design leading to long, contagious lists of thrown exceptions, developers routinely caught and ignored checked exceptions just to fit some exception-throwing API under an interface that did not declare any exception, etc.

Developers stopped using checked exceptions in their APIs. There were wide-spread community calls against checked in Java (see, for example, an article by Stephen Colebourne on Checked Exceptions from as far back as 2010).

However, the fatal blow to checked exceptions came later when lambda expressions were added to Java 8 in 2014 together with Streams and data-processing operators for the Java collections framework. It was so hard to incorporate checked exceptions support there and would have made it so burdensome to use, that checked exceptions support did not make it into the final design. Checked exceptions simply don’t mix well with functional abstractions, they don’t compose well with higher-order functions. Moreover, none of the popular successor languages to Java include checked exceptions support. Checked exceptions are now widely accepted to be a language design dead-end.

Exceptions in Kotlin

Photo by Marc Reichelt on Unsplash

Kotlin inherited the concept of exception from Java and supports them to seamlessly interoperate with JVM libraries. Just like other successor languages, Kotlin learned from Java that checked exceptions should not be a thing. This creates a different problem, though.

It is still called an exception in Kotlin, but it became a different language feature and comes with a different usage style. Moreover, Kotlin developers have to work with Java APIs that use checked exceptions as an alternative way to return a value from a function. How to deal with it and what should exceptions in Kotlin be used for?

Handling program logic errors

The first and foremost use of exceptions in Kotlin is to handle program logic errors. Exceptions are used to check preconditions and invariants in code that static type system cannot track down during compilation. For example, if you a have a function that updates order quantity that must be always positive by the business logic of the code, then you might write something like:

/** Updates order [quanity], must be positive. */
fun updateOrderQuanity(orderId: OrderId, quantity: Int) {
require(quantity > 0) { "Quantity must be positive" }
// proceed with update
}

Calling this function with a negative or zero quantity indicates a logic error in some other place in the code and throws an exception. The same happens when you try to get an element by an out of range index from a list, divide by zero, etc.

As a rule of thumb, you should not be catching exceptions in general Kotlin code. That’s a code smell. Exceptions should be handled by some top-level framework code of your application to alert developers of the bugs in the code and to restart your application or its affected operation. That’s the primary purpose of exceptions in Kotlin.

Dual-use APIs

For some APIs, especially non-domain-specific ones, it is not so clearcut whether the error indicates a program logic error or an error in the input that the application must deal with without calling for the developer’s help. It depends on the context of its use.

Take Kotlin’s String.toInt() extension function for example:

val number = "123".toInt()

In the above code it is pretty clear that a failure of toInt function is a bug in the code and so it throws a NumberFormatException if the given string does not represent an integer. But what if some string came from a user input field and application logic shall deal with invalid input by replacing it with a default value?

Technically, you could handle it by writing try/catch around string.toInt() and it is an idiomatic thing to do in Java. But do not do it in Kotlin. It is a bad style for Kotlin code. There’s a dedicated String.toIntOrNull() extension function specifically created for this purpose. It indicates an error through its result value, using null in this particular case. It nicely interplays with Kotlin null-safety to produce an elegant code that gives a default value in case of malformed input:

val number = string.toIntOrNull() ?: defaultValue

Moreover, the whole Kotlin standard library is designed with this concept in mind. Even operators to get an element from lists and arrays have getOrNull counterparts just in case an index-of-range condition is not a logic error in your particular piece of code, but an intended special case that needs to be handled.

API design

You should design your own general-purpose Kotlin APIs in the same way: use exceptions for logic errors, type-safe results for everything else. Don’t use exceptions as a work-around to sneak a result value out of a function.

When you face an API that is using exceptions for conditions that are not logic errors for your code, then it is better to write a separate wrapper function that converts its exceptions to the appropriate return value and use this wrapper from your code. This way, your caller will have to handle the error condition right away and you avoid writing try/catch in your general application code.

If there’s a single error condition and you are only interested in success or failure of the operation without any details, then prefer using null to indicate a failure. If there are multiple error conditions, then create a sealed class hierarchy to represent various results of your function. Kotlin has all those needs covered by design, including a powerful when expression. This way all the application-specific errors become just values that can be easily and conveniently stored and processed using all the power of Kotlin.

For example, if you’ll ever need to use DateFormat.parse method that throws ParseException with details on errorOffset more than once in your code, then take a bit of time to write the corresponding sealed class and wrapper function and then enjoy Kotlin-style error handling elsewhere:

sealed class ParsedDate {
data class Success(val date: Date) : ParsedDate()
data class Failure(val errorOffset: Int) : ParsedDate()
}

fun DateFormat.tryParse(text: String): ParsedDate =
try {
ParsedDate.Success(parse(text))
} catch (e: ParseException) {
ParsedDate.Failure(e.errorOffset)
}

When you see try/catch in code always eye it with extreme suspicion. Every try/catch in your code has the potential to shuffle a critical programming bug under the rug. Encapsulate them and review them carefully.

Input/output

Let’s get back to the roots and address the largest elephant in the error-handling room — input/output errors. They represent a special case in the error-handling conundrum because they combine two properties. First of all, input/output errors are not logic errors in code. They typically represent external conditions that the code has no control over but must deal with. However, immediately turning every input/output error into a special result value in input/output APIs gets us back to the C state of affairs that we started with. Any kind of parsing or even business logic that makes multiple network requests becomes too cumbersome to write, obscuring the substance of code by the ceremony of error handling.

The default in Kotlin is to use exceptions for input/output. This way you can write dense logic of your code, without having to worry about potential network errors that each individual call could produce:

fun updateOrderQuanity(orderId: OrderId, quantity: Int) {
require(quantity > 0) { "Quantity must be positive" }
val order = loadOrder(orderId)
order.quantity = quantity
storeOrder(order)
}

The special thing about input/output errors is that they are handled in a centralized way by reporting an input/output problem to end-users and/or retrying an operation. You should not be writing boilerplate code to handle a potential network error in each individual network request in your application’s logic. Dedicate a single top-level place in your code to address all network errors uniformly. This is usually done at a boundary between low-level logic of the code and high-level user-interface or service endpoint.

Kotlin exceptions are not checked. One can forget to write this kind of top-level code to handle input/output errors, turning them into logic errors that will get reported as bugs to developers, which is a safe and pragmatic default for many applications.

Exceptions, asynchronous programming, and coroutines

There are already too many words in this story, so we’ll just briefly touch the subject of using exceptions in asynchronous code and coroutines in particular. Kotlin's suspending functions are sequential and thus the simple fact the code is using suspending functions proves nothing special about error handling approach that the code shall use.

However, Kotlin Coroutines are used to build highly asynchronous and concurrent applications where a lot of coroutines are running, each with the potential to fail. Structured Concurrency in Kotlin is designed with the Kotlin’s view of exceptions. All exceptions should automatically percolate to the top-level of the application to get centrally handled. The key design principle is that no exception should be lost, all bugs should get reported to developers.

Thus, the same general advice applies to exceptions and coroutines: don’t use exceptions if you need local handling of certain failure scenarios in your code, don’t use exceptions to return a result value, avoid try/catch in general application code, implement centralized exception-handling logic, handle input/output errors uniformly at an appropriate boundary of your code.

--

--

Roman Elizarov
Roman Elizarov

Written by Roman Elizarov

Project Lead for the Kotlin Programming Language @JetBrains

Responses (23)