Exceptions
The word “exception” is used in the same sense as the phrase “I take exception to that.”
An exceptional condition prevents the continuation of the current function or scope. At the point the problem occurs, you might not know what to do with it, but you cannot continue within the current context. You don’t have enough information to fix the problem. So you must stop and hand the problem to another context that’s able to take appropriate action.
This atom covers the basics of exceptions as an error-reporting mechanism. In Section VI: Preventing Failure, we look at other ways to deal with problems.
It’s important to distinguish an exceptional condition from a normal problem. A normal problem has enough information in the current context to cope with the issue. With an exceptional condition, you cannot continue processing. All you can do is leave, relegating the problem to an external context. This is what happens when you throw an exception. The exception is the object that is “thrown” from the site of the error.
Consider toInt(), which converts a String to an Int. What happens if you
call this function for a String that doesn’t contain an integer value?
// Exceptions/ToIntException.kt
package exceptions
fun erroneousCode() {
// Uncomment this line to get an exception:
// val i = "1$".toInt() // [1]
}
fun main() {
erroneousCode()
}
Uncommenting line [1] produces an exception. Here, the failing line is commented so we don’t stop the book’s build, which checks whether each example compiles and runs as expected.
When an exception is thrown, the path of execution—the one that can’t be
continued—stops, and the exception object ejects from the current
context. Here, it exits the context of erroneousCode() and goes out to the
context of main(). In this case, Kotlin only reports the error; the
programmer has presumably made a mistake and must fix the code.
When an exception isn’t caught, the program aborts and displays a stack trace
containing detailed information. Uncommenting line [1] in
ToIntException.kt, produces the following output:
Exception in thread "main" java.lang.NumberFormatException: For input s\
tring: "1$"
at java.lang.NumberFormatException.forInputString(NumberFormatExcepti\
on.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at ToIntExceptionKt.erroneousCode(at ToIntException.kt:6)
at ToIntExceptionKt.main(at ToIntException.kt:10)
The stack trace gives details such as the file and line where the exception
occurred, so you can quickly discover the issue. The last two lines show the
problem: in line 10 of main() we call erroneousCode(). Then, more precisely,
in line 6 of erroneousCode() we call toInt().
To avoid commenting and uncommenting code to display exceptions, we use the
capture() function from the AtomicTest package:
// Exceptions/IntroducingCapture.kt
import atomictest.*
fun main() {
capture {
"1$".toInt()
} eq "NumberFormatException: " +
"""For input string: "1$""""
}
Using capture(), we compare the generated exception to the expected error
message. capture() isn’t very helpful for normal programming—it’s designed
specifically for this book, so you can see the exception and know that the
output has been checked by the book’s build system.
Another strategy when you can’t successfully produce the expected result is to
return null, which is a special constant denoting “no value.” You can return
null instead of a value of any type. Later in
Nullable Types we discuss the way null affects the type of
the resulting expression.
The Kotlin standard library contains String.toIntOrNull() which performs the
conversion if the String contains an integer number, or produces null if
the conversion is impossible—null is a simple way to indicate failure:
// Exceptions/IntroducingNull.kt
import atomictest.eq
fun main() {
"1$".toIntOrNull() eq null
}
Suppose we calculate average income over a period of months:
// Exceptions/AverageIncome.kt
package firstversion
import atomictest.*
fun averageIncome(income: Int, months: Int) =
income / months
fun main() {
averageIncome(3300, 3) eq 1100
capture {
averageIncome(5000, 0)
} eq "ArithmeticException: / by zero"
}
If months is zero, the division in averageIncome() throws an
ArithmeticException. Unfortunately, this doesn’t tell us anything about why
the error occurred, what the denominator means and whether it can legally be
zero in the first place. This is clearly a bug in the code—averageIncome()
should cope with a months of 0 in a way that prevents a divide-by-zero
error.
Let’s modify averageIncome() to produce more information about the source of
the problem. If months is zero, we can’t return a regular integer value as a
result. One strategy is to return null:
// Exceptions/AverageIncomeWithNull.kt
package withnull
import atomictest.eq
fun averageIncome(income: Int, months: Int) =
if (months == 0)
null
else
income / months
fun main() {
averageIncome(3300, 3) eq 1100
averageIncome(5000, 0) eq null
}
If a function can return null, Kotlin requires that you check the result
before using it (this is covered in Nullable Types). Even if
you only want to display output to the user, it’s better to say “No full month
periods have passed,” rather than “Your average income for the period is:
null.”
Instead of executing averageIncome() with the wrong arguments, you can throw
an exception—escape and force some other part of the program to manage the
issue. You could just allow the default ArithmeticException, but it’s often
more useful to throw a specific exception with a detailed error message. When,
after a couple of years in production, your application suddenly throws an
exception because a new feature calls averageIncome() without properly
checking the arguments, you’ll be grateful for that message:
// Exceptions/AverageIncomeWithException.kt
package properexception
import atomictest.*
fun averageIncome(income: Int, months: Int) =
if (months == 0)
throw IllegalArgumentException( // [1]
"Months can't be zero")
else
income / months
fun main() {
averageIncome(3300, 3) eq 1100
capture {
averageIncome(5000, 0)
} eq "IllegalArgumentException: " +
"Months can't be zero"
}
-
[1] When throwing an exception, the
throwkeyword is followed by the exception to be thrown, along with any arguments it might need. Here we use the standard exception classIllegalArgumentException.
Your goal is to generate the most useful messages possible to simplify the support of your application in the future. Later you’ll learn to define your own exception types and make them specific to your circumstances.
Exercises and solutions can be found at www.AtomicKotlin.com.