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 callingordinal()
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 toannc()
, 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…
- The
main()
function calls theannc()
function.- The
annc()
function then calls theordinal()
function.- The
ordinal()
function calls theget()
function of the list.- The
get()
function returns its result to theordinal()
function.- The
ordinal()
function returns its result to theannc()
function.- The
annc()
function returns its result to themain()
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 theget()
function, at which point each function returns its value in turn, as the program works its way back down the mountain, back into themain()
function.Rather than a drawing of a mountain, a simpler depiction might be a stack of boxes.
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.
And when a function returns, the pile of boxes at the next step will remove that function from the top of the stack.
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.
So, this diagram shows what the call stack looks like at each step as the program is running.
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 themain()
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:
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 number1
, let’s pass it the number9
, which will be out of bounds for theordinal()
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.
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.
Note that, not counting the bottom line of the stack trace, it shows the same thing as our stack of boxes above.
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.
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 calledArrayList
. 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 ourordinal()
function, at line 2 inside a file that I calledMain.kt
.at MainKt.annc(Main.kt:5)
After that,
ordinal()
(from the previous line) was called byannc()
on line 5 ofMain.kt
.at MainKt.main(Main.kt:10)
And
annc()
was called by themain()
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.
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.
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:Let’s add some code to catch an exception! To do this, we’ll use a try expression, which looks like this:
This expression has a few key pieces to it:
- It starts with the
try
keyword.- After that comes the
try
block. This is where we put the exception-prone code.- After that is the
catch
keyword.- 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.
- 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 callannc()
, an exception could end up getting thrown. Since that call is exception-prone, we’ll put it inside thetry
block. Then, inside thecatch
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 isException
.Although the variable name
exception
was used here, most developers prefer using very small variable names for exceptions, such asex
or juste
.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, everyException
object has a nullable property calledmessage
, so let’s update ourprintln()
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 theprintStackTrace()
function.1We’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 thetask
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:
- An exception that is thrown when the task includes the word “clean”.
- 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 ofException
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 generalException
.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 theget()
function of the list.HolidayException
, which could be thrown from ourannc()
function.Each of these is a subclass of the
Exception
class, althoughArrayIndexOutOfBoundsException
goes through a few intermediate classes. Here’s a UML class diagram showing how these classes are related.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 ofException
, including any of its subtypes. BecauseArrayIndexOutOfBoundsException
andHolidayException
are both subtypes ofException
, 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 theArrayIndexOutOfBoundsException
caused by the second task.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.
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.
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 anArrayIndexOutOfBoundsException
, you might want themain()
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
andArrayIndexOutOfBoundsException
are subclasses ofException
, 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, anArrayIndexOutOfBoundsException
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 callingprintln()
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 aString
. To do this, we just remove eachprintln()
, 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 thewords
variable.- If there’s a
HolidayException
, then the string in that block will be assigned towords
. (“It’s a holiday!…”)- If there’s an
ArrayIndexOutOfBoundsException
, then “I can’t count that high!” will be assigned towords
.- 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 aSprinklerBrokenException
, thenfaucet.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 afterwatch(sprinkler)
completes.- If a
SprinklerBrokenException
is thrown, thenfaucet.turnOff()
will run afterstore.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
withtry
, even when you don’t have anycatch
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 aResult
object, which will contain one of two things - either a successful result, or an exception. In the code above, this means…
- If
annc()
completes successfully, then it will contain the result thatannc()
returned.- 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 itsgetOrDefault()
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 thetext
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 themessage
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()
andResult
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 afinally
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
You’ve done an “exceptional” job learning all about exceptions in this chapter, including:
- Why we can end up with problems at runtime.
- What a call stack is, and how to read a stack trace.
- How to catch an exception, and what you can do with it once you’ve got one.
- How to throw our own exceptions.
- How to catch different types of exceptions.
- How try-catch is an expression, so it can be assigned to variables.
- How to use a finally block to make sure something happens, regardless of success or failure.
- How to use runCatching() for a more functional approach to exception-handling.
In the next chapter we’ll finally take a closer look at generic types! See you then!
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. ↩︎
The type of this object isn’t actually
Exception
; it’sThrowable
. See the Throwables and Errors aside above for more information about how they relate to one another. ↩︎