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 do know that 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 an outside context containing code qualified to take appropriate action.

This atom covers the basics of exceptions as an error-reporting mechanism. Later in the book, in Exception Handling, Error Reporting, Logging, and Unit Testing, we’ll look at other ways to discover problems in your code.

It’s important to distinguish an exceptional condition from a normal problem, in which you have enough information in the current context to cope with the difficulty. 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 fun erroneousCode() { // Uncomment this line to get an exception: // val i = "1$".toInt() // [1] } fun main(args: Array<String>) { 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. If you uncomment line [1] in ToIntException.kt, you’ll see the following output:

Exception in thread "main" java.lang.NumberFormatException: For input string: "1$" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at ToIntExceptionKt.erroneousCode(at ToIntException.kt:5) at ToIntExceptionKt.main(at ToIntException.kt:9)

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 9 of main() we call erroneousCode(). Then, more precisely, in line 5 of erroneousCode() we call toInt(). To find the source of the problem you usually need to find the line before the call into a standard library function.

To avoid commenting and uncommenting code in order to display exceptions, we use a function from the AtomicTest package: atomictest.capture(). It stores the exception and compares it to what we expect:

// Exceptions/IntroducingCapture.kt import atomictest.* fun main(args: Array<String>) { capture { "1$".toInt() } eq "NumberFormatException: " + """For input string: "1$"""" }

capture() produces a string containing the type of the exception together with a message describing what went wrong. capture() isn’t very helpful in normal programming—it’s designed specifically for this book, so that 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 instead of that result. null is a special constant denoting “no value.” You can return null instead of a value of any type. Later in Nullable Types we’ll discuss how null affects the type of the resulting expression.

The Kotlin standard library contains String.toIntOrNull() which first checks whether a string contains an integer number, and then it either performs the conversion, or returns null if the conversion is impossible:

// Exceptions/IntroducingNull.kt import atomictest.eq fun main(args: Array<String>) { "1$".toIntOrNull() eq null }

null is a simple way to indicate that something went wrong. You’ll learn later in the book that it is also quite problematic, and is something you generally want to avoid.

Suppose you’re doing something where you might divide by zero—before doing so, you should check for that condition. But what does it mean that the denominator is zero? Maybe you know how to deal with a zero denominator within the problem you’re trying to solve. If it’s an unexpected value, however, you cannot continue that execution path.

Here we calculate an average income over a period of months:

// Exceptions/AverageIncome.kt package firstversion import atomictest.* fun averageIncome(income: Int, months: Int) = income / months fun main(args: Array<String>) { 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 this 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 have a strategy for a months of 0 that prevents a divide-by-zero error.

Let’s modify averageIncome() to produce more information about the source of the problem. One solution is to return null as the result. If months is zero, we can’t return a regular integer value as a result, so we return null instead:

// Exceptions/AverageIncomeWithNull.kt package withnull import atomictest.eq fun averageIncome(income: Int, months: Int) = if (months == 0) null else income / months fun main(args: Array<String>) { averageIncome(3300, 3) eq 1100 averageIncome(5000, 0) eq null }

Kotlin forces you to handle nulls to prevent subsequent indistinguishable errors. Code that uses null forces you to check whether a value is null before you can do something meaningful with it. Even if you want to simply 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 ArithmeticException to be automatically used, but it’s more useful to throw a specific exception with a message containing as much detail as possible. 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 detailed 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(args: Array<String>) { averageIncome(3300, 3) eq 1100 capture { averageIncome(5000, 0) } eq "IllegalArgumentException: " + "Months can't be zero" }

Your goal is to generate the most detailed messages possible to simplify the support of your application in the future.

[1] shows how to throw an exception: the throw keyword followed by the exception to be thrown, along with any arguments it might need. Here we use the standard exception class IllegalArgumentException. Later we’ll show you how to define your own exception types and to make them specific to your circumstances.

Previous          Next

©2018 Mindview LLC. All Rights Reserved.