1. Previous: Chapter 16
  2. Next: Chapter 18
Kotlin: An Illustrated Guide • Chapter 17

Handling Runtime Exceptions

Chapter cover image

In real life, when we decide to do something, we primarily think in terms of a successful experience. For example, if you’re driving to your friend’s house for dinner, you might look up the directions on a mapping website, make sure you’ve got enough gasoline in the car, and leave at the right time in order to arrive when dinner is hot. If everything goes according to plan, you’ll arrive on time.

Things don’t always go according to plan, though. For example, if you get a flat tire while you’re on the way, you’ll have to replace it with the spare, get to a garage, and buy a new tire. By the time you do all of that, dinner could be cold, and you might even have to cancel your plans.

Much like in real life, when our Kotlin code runs, things might not go according to plan. Our functions usually represent our plan - we tell Kotlin, “In general, follow this plan.” If anything goes wrong, then we’ll do something else, as an exception to that plan.

For this reason, when something unexpected happens in our code, we call it an exception. In this chapter, we’ll learn all about how we can handle those exceptions!

Problems at Runtime

We’ve seen how we can get errors at two different times - either at compile time or runtime. As Cecil discovered in the last chapter, errors at compile time are quite helpful, because you can fix them before someone using your application can run into it.

However, not every problem can be detected at compile time. As an example, here’s a function that can convert a number (e.g., 3) to its ordinal (e.g., “third”).

val ordinals = listOf("zeroth", "first", "second", "third", "fourth", "fifth")
fun ordinal(number: Int) = ordinals.get(number)

Looking at this function, it’s easy to see that it only supports ordinals for numbers up to 5. However, there’s no way the Kotlin compiler can be sure that the number argument will be within those bounds. We can easily call this function with a number that’s too high.

fun main() {
    val place = ordinal(9) 
}

Running this main() function results in the following error message.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of 
bounds for length 6
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    at MainKt.ordinal(Main.kt:3)
    at MainKt.main(Main.kt:7)
    at MainKt.main(Main.kt)

The plan was to simply return the ordinal for the number, but things didn’t go according to plan. Passing 9 to this function resulted in an exception to the plan. Since the compiler can’t prevent us from calling ordinal() with an argument that’s out of bounds, we’ll have to handle this problem at runtime instead.

There are many similar problems that can’t be detected at compile time. For example:

  • At compile time, Kotlin can’t know whether a map will include a particular key. (See Listing 9.11).
  • At compile time, Kotlin can’t know what values we might get when we ask a database for data.
  • At compile time, Kotlin can’t know what a user might type into the keyboard when prompted. We could ask the user for a zip code, but might get a phone number instead.

Because of this, we need to know how to handle things that can go wrong at runtime. To do that, we first need to understand the call stack.

The Call Stack

When your Kotlin program is running, code in one place usually calls code in another place, which in turn might call code in yet another place. To demonstrate this, let’s add a function that will announce the ordinal of a task that you’re planning to do. Instead of naming this function announce(), we’ll abbreviate it to annc(), which will fit better on some of the diagrams that we’ll see in a moment.

val ordinals = listOf("zeroth", "first", "second", "third", "fourth", "fifth")
fun ordinal(number: Int) = ordinals.get(number)

fun annc(number: Int, task: String): String {
    val ordinal = ordinal(number)
    return "The $ordinal thing I will do is $task."
}

fun main() {
    val first = annc(1, "clean my room")
    // "The first thing I will do is clean my room."
}

This code has a simple execution path. Starting in the main() function…

  1. The main() function calls the annc() function.
  2. The annc() function then calls the ordinal() function.
  3. The ordinal() function calls the get() function of the list.
  4. The get() function returns its result to the ordinal() function.
  5. The ordinal() function returns its result to the annc() function.
  6. The annc() function returns its result to the main() function.

And of course, when the main() function ends, the program successfully finishes running.

It’s kind of like our program is climbing a mountain. It starts inside the main() function at ground level, and for each function call along the way, it climbs higher on the mountain. Eventually, it’s in the get() function, at which point each function returns its value in turn, as the program works its way back down the mountain, back into the main() function.

Each function calls the next, and each function returns to the previous - much like ascending and descending a mountain.

Rather than a drawing of a mountain, a simpler depiction might be a stack of boxes.

Call stacks, from left to right, as the program is running. main() annc() ordinal() ordinal() ordinal() get() annc() annc() annc() annc() main() main() main() main() main() main() time

The diagram above still roughly has the shape of a mountain. Each little box represents a function. As the program’s execution progresses (from left to right in this diagram), each function either calls another function, or it returns a value to the function that called it.

When one function calls another, then the pile of boxes at the next step will include that next function on top of the function that called it.

Calling a function adds to the top of the stack. ordinal() calls get() main() ordinal() annc() main() ordinal() get() annc()

And when a function returns, the pile of boxes at the next step will remove that function from the top of the stack.

Returning from a function removes the box at the top of the stack. main() ordinal() annc() main() ordinal() get() annc() get() returns to ordinal()

Note that those are the only two ways that the stack will change! A box can never be added or removed from anywhere except the top of the stack! In programming, this stack of function calls is referred to as the call stack, or sometimes just the stack. Each box in the stack is a stack frame.

Clarifies the terminology around call stacks and stack frames. main() ordinal() annc() stack frame stack frame stack frame call stack

So, this diagram shows what the call stack looks like at each step as the program is running.

The call stack over time, at each step along the way. Step 1 Step 2 Step 3 Step 4 Step 5 Step 6 Step 7 main() annc() ordinal() ordinal() ordinal() get() annc() annc() annc() annc() main() main() main() main() main() main()

Our example from Listing 17.3 is pretty simple - we just have a few functions, each calling the next, so over time, the call stack looks like a single mountain. Usually, though, it’ll look more like a mountain range. For example, let’s add a call to println() in the main() function.

fun main() {
    val first = annc(1, "clean my room")
    println(first)
}
The first thing I will do is clean my room.

With this small change, the call stack over time looks like this:

The call stack over time, at each step along the way, with an extra call to `println()`. Step 8 Step 9 Step 1 Step 2 Step 3 Step 4 Step 5 Step 6 Step 7 main() annc() ordinal() ordinal() ordinal() get() annc() annc() annc() annc() main() main() main() main() main() main() main() main() println()

For what it’s worth, the println() function itself technically calls other functions, so if we were to fully chart out all of those calls, the mountain range would look even more complex than it does above!

One last thing to note before we move on - in Listing 17.3, we’ve only got functions calling other functions, but a call stack will often include other things. For example, when you call a class’ constructor, the properties of that class will also be initialized, and when that happens, it’s also part of the call stack. Similarly, you might get a value from a property that has a custom getter function. These would also have their own frames on the call stack.

So now that we know all about the call stack, what does this have to do with exceptions and error messages?

Call Stacks, Exceptions, and Error Messages

Let’s update the main() function. Instead of passing it the number 1, let’s pass it the number 9, which will be out of bounds for the ordinal() function.

fun main() {
    val task = annc(9, "clean my room")
    println(task)
}

When this runs, instead of seeing “The ninth thing I will do is clean my room”, we get an ArrayIndexOutOfBoundsException, just like we did back in Listing 17.2. Here’s that error message again:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 
Index 9 out of bounds for length 6
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    at MainKt.ordinal(Main.kt:2)
    at MainKt.annc(Main.kt:5)
    at MainKt.main(Main.kt:10)
    at MainKt.main(Main.kt)

Let’s examine how the call stack changes over time as this code is running.

Call stack over time, and the exception happens in the `get()` function. main() annc() ordinal() ordinal() get() annc() annc() main() main() main()

When things blow up, the call stack has four frames on it. The error message that we get includes a snapshot of what the call stack looked like at the time that the exception happened. A snapshot of the call stack at a particular point in time is known as a stack trace.

Anatomy of a Kotlin error message. Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6 at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) at MainKt.ordinal(Main.kt:2) at MainKt.annc(Main.kt:5) at MainKt.main(Main.kt:10) at MainKt.main(Main.kt) ... happened in this function stack trace This exception...

Note that, not counting the bottom line of the stack trace, it shows the same thing as our stack of boxes above.

Shows the similarity between the stack trace in the error message and the visualized call stack. ordinal() get() annc() main() at java.baseget(Arrays.java:4165) at MainKt.ordinal(Main.kt:2) at MainKt.annc(Main.kt:5) at MainKt.main(Main.kt:10)

For each frame in the stack trace, we can see the name of the function, the name of the file where that function is, and the line number of code where the next function was called.

How to interpret each line of a stack trace. at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) at MainKt.ordinal(Main.kt:2) at MainKt.annc(Main.kt:5) at MainKt.main(Main.kt:10) at MainKt.main(Main.kt) at MainKt.annc(Main.kt:5) function name filename line number

It’s easy to understand the stack trace, but let’s take it one line at a time.

at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)

This line tells us that the error happened in a function called get(), which is inside a class called ArrayList. This class belongs to Java’s API, which was used by Kotlin’s standard library, and the error happened at line 4165. (Yes, that’s a lot of lines!)

at MainKt.ordinal(Main.kt:2)

Next, we can see that the get() function (from the previous line) was called from our ordinal() function, at line 2 inside a file that I called Main.kt.

at MainKt.annc(Main.kt:5)

After that, ordinal() (from the previous line) was called by annc() on line 5 of Main.kt.

at MainKt.main(Main.kt:10)

And annc() was called by the main() function on line 10 of the same file.

at MainKt.main(Main.kt)

And this final line simply shows us that everything started in the Main.kt file.

The stack trace can be helpful when you’re trying to figure out what went wrong, but it also shows us where we could add code to try to prevent the program from abruptly terminating.

Catching Exceptions

When an exception arises, it’s kind of like a kid throwing a baseball toward a window. If nobody is there to catch that baseball, it’ll crash into the window.

Baseball player, throwing a baseball into a window.

Similarly, in our Kotlin code, when an exception is thrown, if nobody catches the exception, an error message will be printed out, and the program will crash.

A baseball being thrown is much like an exception being thrown in Kotlin - if nobody catches it, there will be a crash.

Usually, we do not want our program to crash in response to an exception. To demonstrate why, let’s replace our single task with a list of tasks, and print out an announcement for each one.

fun main() {
    val tasks = listOf(1 to "clean my room", 9 to "take out trash", 5 to "feed the dog")
    tasks.forEach { (number, task) -> 
        println(annc(number, task)) 
    }
}

When this runs, we’ll see the following output:

The first thing I will do is clean my room.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    at MainKt.ordinal(Main.kt:2)
    at MainKt.annc(Main.kt:5)
    at MainKt.main(Main.kt:11)
    at MainKt.main(Main.kt)

The first task was printed to the screen, but the second task caused an exception, so the program aborted. Because the program stopped running, it never printed out the task about feeding the dog! Instead, it’d be great if the program would keep running, even when there’s an exception.

To keep the things running, we’ll need to catch that exception. In other words, we need someone to catch that baseball before it crashes through the window!

So, how can we catch an exception? Well, we can put a baseball player at any frame of the call stack. Starting from the frame where it’s thrown, the exception will pass by each stack frame, one by one, all the way through the main() function where the program started. At each step along the way, if there’s a ball player in that frame, he or she can catch the exception, preventing the window from breaking.

For example, we can put a catcher in the main() function, and she’ll catch the exception just before it hits the window:

Catching the exception baseball prevents a crash.

Let’s add some code to catch an exception! To do this, we’ll use a try expression, which looks like this:

Anatomy of a try expression in Kotlin. try { do something exception-prone here } catch (exception: Exception) { handle the exception here } "try" keyword "catch" keyword exception parameter

This expression has a few key pieces to it:

  1. It starts with the try keyword.
  2. After that comes the try block. This is where we put the exception-prone code.
  3. After that is the catch keyword.
  4. Inside the parentheses is the exception parameter, which is declared with a name and the type of the exception that we want to catch. We’ll learn more about this in a moment.
  5. After that comes the catch block. This is where we put the code that should run when an exception is caught.

To start with, let’s just put our catcher (that is, a try expression) in the main() function, where she can catch the exception just before it breaks the window. When we call annc(), an exception could end up getting thrown. Since that call is exception-prone, we’ll put it inside the try block. Then, inside the catch block, we’ll simply print out that something went wrong. Here’s what we end up with:

fun main() {
    val tasks = listOf(1 to "clean my room", 9 to "take out trash", 3 to "feed the dog")
    tasks.forEach { (number, task) -> 
        try {
            println(annc(number, task)) 
        } catch (exception: Exception) {
            println("Something went wrong!")
        }
    }
}

When we run this again, we’ll still get an exception while processing “take out trash”, but this time, the exception will be handled by the catch block. As a result, we see this:

The first thing I will do is clean my room.
Something went wrong!
The fifth thing I will do is feed the dog.

With this change, we prevented the program from crashing with an error message! That means we still get to see the task about feeding the dog! After processing all three tasks, the program ends successfully.

Printing out “Something went wrong!” is better than nothing, but there’s much more we can do with exceptions when we catch them.

Exceptions Are Objects

An exception is an object, and we can use its functions and properties, just like any other object. In Listing 17.7 above, our catch block declared an exception parameter named exception and that parameter’s type is Exception.

An exception parameter that has the Exception type. try { println ( announcement (number, task)) } catch (exception: Exception) { println ( "Something went wrong!" ) }

Although the variable name exception was used here, most developers prefer using very small variable names for exceptions, such as ex or just e.

try {
    println(annc(number, task)) 
} catch (e: Exception) {
    println("Something went wrong!")
}

This variable is visible inside the catch block, so you can interact with it as needed. For example, every Exception object has a nullable property called message, so let’s update our println() to include that message.

try {
    println(annc(number, task)) 
} catch (e: Exception) {
    println("Something went wrong! ${e.message}")
}

Now when this runs, instead of just seeing that something went wrong, we can see what it was that went wrong - in this case, that index 9 was out of bounds for an array that has a length of 6.

The first thing I will do is clean my room.
Something went wrong! Index 9 out of bounds for length 6
The fifth thing I will do is feed the dog.

In addition to the message property, Exception objects include a stack trace, which you can print out with the printStackTrace() function.1

We’re not confined to using the Exception class itself, though. We can extend it, and include any functions or properties that we want! Before that will be helpful, though, we’ll first need to learn how to throw our own exceptions.

Throwing Exceptions

In addition to catching exceptions, we can also throw them ourselves. Since an exception is just an object, we can instantiate one like usual - by calling its constructor. We don’t have to pass any arguments to its constructor, but it can be helpful to give it a message describing the problem.

val exception = Exception("No cleaning allowed on holidays!")

Once you have an instance of an exception, you can throw the exception with the throw keyword, so we could do this:

throw exception

However, the stack trace of an exception is determined at the point where the exception is instantiated, not the point at which it is thrown. For that reason, we don’t usually instantiate and store an exception in a variable like we’re doing in Listing 17.10. Instead, it’s typical to instantiate an exception when you throw it, like this.

throw Exception("No cleaning allowed on holidays!")

Let’s update our annc() function so that if the task includes the word “clean”, then we’ll throw this exception.

fun annc(number: Int, task: String): String {
    if ("clean" in task) throw Exception("No cleaning allowed on holidays!")
    val ordinal = ordinal(number)
    return "The $ordinal thing I will do is $task."
}

Running this with the try expression from Listing 17.9 results in this output:

Something went wrong! No cleaning allowed on holidays!
Something went wrong! Index 9 out of bounds for length 6
The fifth thing I will do is feed the dog.

Our try expression is now handling at least two different cases of exceptions:

  1. An exception that is thrown when the task includes the word “clean”.
  2. An exception that is thrown when the index is out of bounds.

At the moment, we’re handling both of those exceptions the same way, but we might prefer to handle each of those cases differently. To distinguish between these two different kinds of exceptions, we can make sure they have different types.

Exception Types

Although we can instantiate the Exception class directly, as we did in Listing 17.13, it’s often helpful to extend it for each unique category of exception that we throw. For example, we can create a subclass of Exception that’s dedicated to holidays, when we aren’t allowed to clean.

class HolidayException(val task: String) : Exception("'$task' is not allowed on holidays")

Now we can throw a HolidayException instead of a general Exception.

fun annc(number: Int, task: String): String {
    if ("clean" in task) throw HolidayException(task)
    // ...
}

With this change, this program now deals with two kinds of exceptions:

  • ArrayIndexOutOfBoundsException, which could be thrown from the get() function of the list.
  • HolidayException, which could be thrown from our annc() function.

Each of these is a subclass of the Exception class, although ArrayIndexOutOfBoundsException goes through a few intermediate classes. Here’s a UML class diagram showing how these classes are related.

UML class diagram illustrating the hierarchy of exception classes in Kotlin, including Exception, RuntimeException, IndexOutOfBoundsException, ArrayIndexOutOfBoundsException, and a custom HolidayException. HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException

Let’s look at our try expression again.

try {
    println(annc(number, task)) 
} catch (e: Exception) {
    println("Something went wrong! ${e.message}")
}

The type of the exception parameter tells Kotlin which kinds of exceptions should be handled in this catch block. This is like telling the ball players to only catch balls of a certain color, and ignore the others.

In Listing 17.16, the type is Exception, so this catch block will handle any exceptions that have a type of Exception, including any of its subtypes. Because ArrayIndexOutOfBoundsException and HolidayException are both subtypes of Exception, this code will catch both of those exceptions.

However, we can change this type to something more specific. For example, let’s change it to HolidayException.

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("Something went wrong! ${e.message}")
}

After making this change, we can run the code, and see in our output that the try expression caught and handled the HolidayException, but it did not catch the ArrayIndexOutOfBoundsException caused by the second task.

Output from running the code, indicating which exceptions were handled. Something went wrong! 'clean my room' is not allowed on holidays Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6 (stack trace follows) this exception was handled this exception was not handled

So, the type of the exception parameter determines which exceptions are caught. Just remember that the catch block will handle the specified type, including any of its subtypes.

A catch block for Exception will include include Exception and all of its subtypes. HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException ... will catch all of these exception types: try { } catch (e: Exception) { } This code... A catch block for HolidayException will include only HolidayException, since it has no subtypes. HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException ... will catch just this exception type: try { } catch (e: HolidayException) { } This code... A catch block for RuntimeException will include include RuntimeException and all of its subtypes. HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException ... will catch these exception types: try { } catch (e: RuntimeException) { } This code...

Any exception types that the catch block does not catch will simply continue to the next stack frame, where another try expression could have a chance to catch and handle it. As always, if nobody catches the exception, it’ll crash through the window.

If no catch block catches the exception, then there will be a crash.

Handling Multiple Exception Types Differently

Sometimes you might want to handle multiple types of exceptions in a single try expression, but do something different with each one of them. For example, instead of catching a HolidayException and ignoring an ArrayIndexOutOfBoundsException, you might want the main() function to handle them both, but print out something different in each case.

To do this, we can simply append additional catch blocks as needed, like this.

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
}
It's a holiday! I'm not going to clean my room today!
I can't count that high!
The fifth thing I will do is feed the dog.

Note that when there’s more than one catch block, the first block with the matching exception type wins - and any following blocks will be disregarded, even if the type of its exception parameter matches. Because of this, it’s a good idea to think about the order of the catch blocks. For example, we could add a catch block for Exception at the very top:

try {
    println(annc(number, task))
} catch (e: Exception) {
    println("I wasn't expecting this!")
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
}

However, if we do this, the two catch blocks that follow will be pointless. Since HolidayException and ArrayIndexOutOfBoundsException are subclasses of Exception, they’ll always only match the first block!

I wasn't expecting this!
I wasn't expecting this!
The fifth thing I will do is feed the dog.

So instead, if a single try expression is going to include cases for both an exception class and one or more of its subclasses, put the subclasses above their corresponding superclasses, like this:

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
} catch (e: Exception) {
    println("I wasn't expecting this!")
}

With this change, a HolidayException will be handled in the first catch block, an ArrayIndexOutOfBoundsException will be handled in the second, and any other kind of exception will be handled in the third block.

Evaluating a Try Expression

Throughout this chapter, we’ve referred to try-catch as an expression. As you might recall, the difference between a statement and an expression is that an expression can be evaluated - in other words, it can be reduced to a value. Since try expressions are indeed expressions, we can assign them to variables, pass them to functions, or return them from functions.

For example, currently, our try expression in the main() function is calling println() in each case.

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
}

Instead of calling println() everywhere, we can simply let this try expression evaluate to a String. To do this, we just remove each println(), and assign the result to a variable. Then we can print that variable.

val words: String = try {
    annc(number, task)
} catch (e: HolidayException) {
    "It's a holiday! I'm not going to ${e.task} today!"
} catch (e: ArrayIndexOutOfBoundsException) {
    "I can't count that high!"
}

println(words)

This works exactly like you’d expect:

  • If there are no exceptions, the result of annc() will be assigned to the words variable.
  • If there’s a HolidayException, then the string in that block will be assigned to words. (“It’s a holiday!…”)
  • If there’s an ArrayIndexOutOfBoundsException, then “I can’t count that high!” will be assigned to words.
  • If there’s any other exception, it won’t be caught here.

Try-Catch-Finally

One day, there was a friendly neighbor named Tara. She takes pride in having a lush, green, well-manicured lawn. Twice a week, she would turn on the outdoor faucet to start watering her lawn. One day, after turning on the faucet, she noticed that the sprinkler wasn’t spinning like it should.

Realizing that the sprinker was broken, she went into the house and ordered a new one from an online store. Then, she continued with everything else that she needed to do that day. Unfortunately, by the end of the day, her lawn was flooded, because she never turned off the faucet!

Just like Tara, sometimes you need to make sure you wrap up a task, even if something goes wrong! The following code models Tara’s experience:

try {
    faucet.turnOn()
    watch(sprinkler) // SprinklerBrokenException is thrown here
    faucet.turnOff() // this never runs!
} catch (e: SprinklerBrokenException) {
    store.orderNewSprinker()
}

This code seems reasonable, but there’s a problem with it - if the watch() function throws a SprinklerBrokenException, then faucet.turnOff() never runs!

To help with cases like this, you can include a block called finally at the end of a try expression. A finally block will run after the rest of the expression is processed, regardless of whether an exception was thrown. Here’s how it looks:

try {
    faucet.turnOn()
    watch(sprinkler) // SprinklerBrokenException is thrown here
} catch (e: SprinklerBrokenException) {
    store.orderNewSprinker()
} finally {
    faucet.turnOff() // This will run, even when the sprinkler breaks!
}

With this code:

  • If no exception is thrown, then faucet.turnOff() will run after watch(sprinkler) completes.
  • If a SprinklerBrokenException is thrown, then faucet.turnOff() will run after store.orderNewSprinkler() runs.
  • If any other kind of exception is thrown, then faucet.turnOff() will run before the exception makes its way toward the start of the stack.

A finally block is typically most helpful with resources that need to be closed. For example, if you want your program to read a file on your computer, you’ll want to make sure that you close it when you’re done with it, even if an exception is thrown while processing it.

Note that you can use finally with try, even when you don’t have any catch blocks! For example, you might want the exception to be handled by another try-expression, closer to the start of the stack, but still need to make sure you turn the faucet off.

try {
    faucet.turnOn()
    watch(sprinkler)
} finally {
    faucet.turnOff()
}

At minimum, a try expression must have a catch block or a finally block.

Try expressions are a bit verbose. They tend to take up a lot of space on the screen, and they result in an execution path that can be difficult to follow, because it’s not always obvious where an exception will be handled. Kotlin includes another approach to exception handling, which is also worth considering.

A Functional Approach to Exception Handling

Kotlin’s standard library includes a function named runCatching(). Internally, it uses a try-expression, but by using this function, we can often avoid manually writing a try-expression in our own code. Using this function is easy. To start with, simply call it with the exception-prone code, and assign its result to a variable, like this:

fun main() {
    val tasks = listOf(1 to "clean my room", 9 to "take out trash", 5 to "feed the dog")
    tasks.forEach { (number, task) -> 
        val result = runCatching { annc(number, task) } 
    }
}

runCatching() will return a Result object, which will contain one of two things - either a successful result, or an exception. In the code above, this means…

  1. If annc() completes successfully, then it will contain the result that annc() returned.
  2. If annc() throws an exception, then it will contain that exception.

The Result object has a few functions on it that can be used to work with the result or exception. An easy way to work with it is to use its getOrDefault() function. For example, the following code does the same thing as Listing 17.8:

val result = runCatching { annc(number, task) }
val text = result.getOrDefault("Something went wrong!")
println(text)

In this code, if annc() completes successfully, then its result will be assigned to the text variable. Otherwise the string “Something went wrong!” will be assigned to it. This works just fine in many cases, but if we need the default to be based on a property of the exception - for example, if we want to include the message of the exception - then we’ll need something else.

For cases like these, we can use getOrElse(). This function takes a lambda where you can operate on the exception object.2 For example, the code below works the same as Listing 17.9.

val result = runCatching { annc(number, task) }
val text = result.getOrElse { "Something went wrong! ${it.message}" }
println(text)

The Result class includes a number of other functions that can be used to transform its value or exception. These functions can be chained together, much like a collection operation chain.

runCatching() and Result can make some code easier to read than their equivalent try-expressions. However, if we write the try-expression ourselves, we get more control - we can catch specific kinds of exceptions, and can add a finally block if it’s needed.

Also, developers who come from a Functional Programming background often want more advanced functionality, such as the ability to catch only certain kinds of exceptions, or the ability to accumulate multiple exceptions. If that’s you, you’ll probably want to check out the Arrow Functional Programming libraries for Kotlin.

Summary

Enjoying this book?
Pick up the Leanpub edition today!

Kotlin: An Illustrated Guide is now available on Leanpub See the book on Leanpub

You’ve done an “exceptional” job learning all about exceptions in this chapter, including:

In the next chapter we’ll finally take a closer look at generic types! See you then!


  1. This function is available when your Kotlin code is targeting the Java Virtual Machine or a native platform, but not currently available when targeting JavaScript. ↩︎

  2. The type of this object isn’t actually Exception; it’s Throwable. See the Throwables and Errors aside above for more information about how they relate to one another. ↩︎

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit