Catching Exceptions
You can regain control over a program in which an exception has been thrown by catching that exception. Kotlin provides direct and indirect ways of doing this.
try…catch
The direct way of catching exceptions involves putting code that might
cause an exception inside a try block. This block is immediately followed
by one or more catch blocks:
try {
// code that might cause an exception
}
catch (error: MyException) {
// specific exception handling code
}
catch (error: Exception) {
// more general exception handling code
}
Each catch block specifies the type of exception that it will intercept.
The code inside the block specifies your response to the matching exception.
The catch blocks are processed in the order in which they appear, with the
first matching block being executed and all subsequent blocks being ignored
(even if they would also match). If no match is found, the exception will
continue to propagate outwards.
An important point to understand is that a catch block written to
intercept an instance of a particular class of exception will also intercept
instances of subclasses.
So, referring to the earlier diagram, a catch block that intercepts
IllegalArgumentException will also intercept NumberFormatException, and
a block that intercepts Exception will catch all types of exception.
Let’s see how this works in practice.
-
Open the file
Catch.kt, in thetasks/task9_4subdirectory of your repository. Study the code carefully. -
Compile
Catch.kt, then run the program with a variety of inputs to see how it behaves. Use your observations to help you answer the questions in the following quiz.
Exception Handling Tips
-
Don’t micromanage exceptions.
Whilst it might seem tempting to give each potentially exception-causing function call its own enclosing
tryandcatch, this will quickly make your code cluttered and hard to read. -
Organize
catchblocks so that the more specialized exception types are caught first.Remember that a
catchblock that targetsExceptionwill catch all kinds of exception. If you have multiplecatchblocks, it should be the last one. -
Don’t throw an exception yourself and then immediately catch it.
Remember: throwing an exception at one point in your code signals that you don’t know how to deal with an error (or that you don’t want to make assumptions about how to deal with it). Catching the exception should happen at a point where you are able to take on the responsibility for handling the error.
These two points should be at different locations in your code. If you find yourself tempted to signal an error and handle it within the same context, then there really isn’t any point in throwing the exception in the first place!
finally
You can optionally follow your catch blocks with a finally block. The
code in this block will always execute, regardless of whether an exception
has occurred in the try block. This can be useful when working with
files or network connections, as you can then ensure that these have been
properly closed.
try {
// code that might cause an exception
}
catch (error: MyException) {
// code that runs if MyException occurs
}
finally {
// code that always runs, with or without an exception
}
It’s also possible to pair try directly with finally, i.e., have no
catch blocks at all. The try…finally structure is useful if you
don’t want to catch any exceptions but want to guarantee the clean-up of
resources1.
You can never have a try block on its own. It must always be followed
immediately by a catch block, or a finally block.
runCatching()
The runCatching() function runs the given block of code, catching
any exceptions that occur within that block. It returns a Result
object that encapsulates any value returned from that block of code, and
details of the caught exception (if one was thrown).
You can access the isSuccess or isFailure boolean properties to see
whether the block of code ran successfully, or failed with an exception.
You can retrieve the value returned from the block of code2 using the
method getOrNull(). This value will be null if execution of the block
failed and an exception was caught.
You can retrieve the caught exception using the method exceptionOrNull().
This will yield null if execution succeeded and no exception was caught.
Here’s an example of how you might use runCatching():
val result = runCatching {
// code that could produce exceptions
}
if (result.isFailure) {
println("Task failed with an exception:")
println(result.exceptionOrNull())
}
In this example, the message associated with the caught exception is printed, but you could do something more sophisticated here—e.g., retrieve the full stack trace and write it to a log file.
runCatching() doesn’t do anything you couldn’t do already using try and
catch blocks. It’s main benefit lies in making your code easier to read.