Exceptions
Exceptions help your code run more predictably, even when runtime errors occur that could disrupt program execution. Kotlin treats all exceptions as unchecked by default. Unchecked exceptions simplify the exception handling process: you can catch exceptions, but you don't need to explicitly handle or declare them.
Working with exceptions consists of two primary actions:
Throwing exceptions: indicate when a problem occurs.
Catching exceptions: handle the unexpected exception manually by resolving the issue or notifying the developer or application user.
Exceptions are represented by subclasses of the Exception
class, which is a subclass of the Throwable
class. For more information about the hierarchy, see the Exception hierarchy section. Since Exception
is an open class
, you can create custom exceptions to suit your application's specific needs.
Throw exceptions
You can manually throw exceptions with the throw
keyword. Throwing an exception indicates that an unexpected runtime error has occurred in the code. Exceptions are objects, and throwing one creates an instance of an exception class.
You can throw an exception without any parameters:
To better understand the source of the problem, include additional information, such as a custom message and the original cause:
In this example, an IllegalArgumentException
is thrown when the user inputs a negative value. You can create custom error messages and keep the original cause (cause
) of the exception, which will be included in the stack trace.
Throw exceptions with precondition functions
Kotlin offers additional ways to automatically throw exceptions using precondition functions. Precondition functions include:
Precondition function | Use case | Exception thrown |
---|---|---|
Checks user input validity | ||
Checks object or variable state validity | ||
Indicates an illegal state or condition |
These functions are suitable for situations where the program's flow cannot continue if specific conditions aren't met. This streamlines your code and makes handling these checks efficient.
require() function
Use the require()
function to validate input arguments when they are crucial for the function's operation, and the function can't proceed if these arguments are invalid.
If the condition in require()
is not met, it throws an IllegalArgumentException
:
check() function
Use the check()
function to validate the state of an object or variable. If the check fails, it indicates a logic error that needs to be addressed.
If the condition specified in the check()
function is false
, it throws an IllegalStateException
:
error() function
The error()
function is used to signal an illegal state or a condition in the code that logically should not occur. It's suitable for scenarios when you want to throw an exception intentionally in your code, such as when the code encounters an unexpected state. This function is particularly useful in when
expressions, providing a clear way to handle cases that shouldn't logically happen.
In the following example, the error()
function is used to handle an undefined user role. If the role is not one of the predefined ones, an IllegalStateException
is thrown:
Handle exceptions using try-catch blocks
When an exception is thrown, it interrupts the normal execution of the program. You can handle exceptions gracefully with the try
and catch
keywords to keep your program stable. The try
block contains the code that might throw an exception, while the catch
block catches and handles the exception if it occurs. The exception is caught by the first catch
block that matches its specific type or a superclass of the exception.
Here's how you can use the try
and catch
keywords together:
It's a common approach to use try-catch
as an expression, so it can return a value from either the try
block or the catch
block:
You can use multiple catch
handlers for the same try
block. You can add as many catch
blocks as needed to handle different exceptions distinctively. When you have multiple catch
blocks, it's important to order them from the most specific to the least specific exception, following a top-to-bottom order in your code. This ordering aligns with the program's execution flow.
Consider this example with custom exceptions:
A general catch block handling WithdrawalException
, catches all exceptions of its type, including specific ones like InsufficientFundsException
, unless they are caught earlier by a more specific catch block.
The finally block
The finally
block contains code that always executes, regardless of whether the try
block completes successfully or throws an exception. With the finally
block you can clean up code after the execution of try
and catch
blocks. This is especially important when working with resources like files or network connections, as finally
guarantees they are properly closed or released.
Here is how you would typically use the try-catch-finally
blocks together:
The returned value of a try
expression is determined by the last executed expression in either the try
or catch
block. If no exceptions occur, the result comes from the try
block; if an exception is handled, it comes from the catch
block. The finally
block is always executed, but it doesn't change the result of the try-catch
block.
Let's look at an example to demonstrate:
If your code requires resource cleanup without handling exceptions, you can also use try
with the finally
block without catch
blocks:
As you can see, the finally
block guarantees that the resource is closed, regardless of whether an exception occurs.
In Kotlin, you have the flexibility to use only a catch
block, only a finally
block, or both, depending on your specific needs, but a try
block must always be accompanied by at least one catch
block or a finally
block.
Create custom exceptions
In Kotlin, you can define custom exceptions by creating classes that extend the built-in Exception
class. This allows you to create more specific error types tailored to your application's needs.
To create one, you can define a class that extends Exception
:
In this example, there is a default error message, "My message", but you can leave it blank if you want.
Custom exceptions can also be a subclass of any pre-existent exception subclass, like the ArithmeticException
subclass:
Custom exceptions behave just like built-in exceptions. You can throw them using the throw
keyword, and handle them with try-catch-finally
blocks. Let's look at an example to demonstrate:
In applications with diverse error scenarios, creating a hierarchy of exceptions can help making the code clearer and more specific. You can achieve this by using an abstract class or a sealed class as a base for common exception features and creating specific subclasses for detailed exception types. Additionally, custom exceptions with optional parameters offer flexibility, allowing initialization with varied messages, which enables more granular error handling.
Let's look at an example using the sealed class AccountException
as the base for an exception hierarchy, and class APIKeyExpiredException
, a subclass, which showcases the use of optional parameters for improved exception detail:
The Nothing type
In Kotlin, every expression has a type. The type of the expression throw IllegalArgumentException()
is Nothing
, a built-in type that is a subtype of all other types, also known as the bottom type. This means Nothing
can be used as a return type or generic type where any other type is expected, without causing type errors.
Nothing
is a special type in Kotlin used to represent functions or expressions that never complete successfully, either because they always throw an exception or enter an endless execution path like an infinite loop. You can use Nothing
to mark functions that are not yet implemented or are designed to always throw an exception, clearly indicating your intentions to both the compiler and code readers. If the compiler infers a Nothing
type in a function signature, it will warn you. Explicitly defining Nothing
as the return type can eliminate this warning.
This Kotlin code demonstrates the use of the Nothing
type, where the compiler marks the code following the function call as unreachable:
Kotlin's TODO()
function, which also uses the Nothing
type, serves as a placeholder to highlight areas of the code that need future implementation:
As you can see, the TODO()
function always throws a NotImplementedError
exception.
Exception classes
Let's explore some common exception types found in Kotlin, which are all subclasses of the RuntimeException
class:
ArithmeticException
: This exception occurs when an arithmetic operation is impossible to perform, like division by zero.val example = 2 / 0 // throws ArithmeticExceptionIndexOutOfBoundsException
: This exception is thrown to indicate that an index of some sort, such as an array or string is out of range.val myList = mutableListOf(1, 2, 3) myList.removeAt(3) // throws IndexOutOfBoundsExceptionNoSuchElementException
: This exception is thrown when an element that does not exist in a particular collection is accessed. It occurs when using methods that expect a specific element, such asfirst()
,last()
, orelementAt()
.val emptyList = listOf<Int>() val firstElement = emptyList.first() // throws NoSuchElementExceptionNumberFormatException
: This exception occurs when attempting to convert a string to a numeric type, but the string doesn't have an appropriate format.val string = "This is not a number" val number = string.toInt() // throws NumberFormatExceptionNullPointerException
: This exception is thrown when an application attempts to use an object reference that has thenull
value. Even though Kotlin's null safety features significantly reduce the risk of NullPointerExceptions, they can still occur either through deliberate use of the!!
operator or when interacting with Java, which lacks Kotlin's null safety.val text: String? = null println(text!!.length) // throws a NullPointerException
While all exceptions are unchecked in Kotlin, and you don't have to catch them explicitly, you still have the flexibility to catch them if desired.
Exception hierarchy
The root of the Kotlin exception hierarchy is the Throwable
class. It has two direct subclasses, Error
and Exception
:
The
Error
subclass represents serious fundamental problems that an application might not be able to recover from by itself. These are problems that you generally would not attempt to handle, such asOutOfMemoryError
orStackOverflowError
.The
Exception
subclass is used for conditions that you might want to handle. Subtypes of theException
type, such as theRuntimeException
andIOException
(Input/Output Exception), deal with exceptional events in applications.
RuntimeException
is usually caused by insufficient checks in the program code and can be prevented programmatically. Kotlin helps prevent common RuntimeExceptions
like NullPointerException
and provides compile-time warnings for potential runtime errors, such as division by zero. The following picture demonstrates a hierarchy of subtypes descended from RuntimeException
:
Stack trace
The stack trace is a report generated by the runtime environment, used for debugging. It shows the sequence of function calls leading to a specific point in the program, especially where an error or exception occurred.
Let's see an example where the stack trace is automatically printed because of an exception in a JVM environment:
Running this code in a JVM environment produces the following output:
The first line is the exception description, which includes:
Exception type:
java.lang.ArithmeticException
Thread:
main
Exception message:
"This is an arithmetic exception!"
Each other line that starts with an at
after the exception description is the stack trace. A single line is called a stack trace element or a stack frame:
at MainKt.main (Main.kt:3)
: This shows the method name (MainKt.main
) and the source file and line number where the method was called (Main.kt:3
).at MainKt.main (Main.kt)
: This shows that the exception occurs in themain()
function of theMain.kt
file.
Exception interoperability with Java, Swift, and Objective-C
Since Kotlin treats all exceptions as unchecked, it can lead to complications when such exceptions are called from languages that distinguish between checked and unchecked exceptions. To address this disparity in exception handling between Kotlin and languages like Java, Swift, and Objective-C, you can use the @Throws
annotation. This annotation alerts callers about possible exceptions. For more information, see Calling Kotlin from Java and Interoperability with Swift/Objective-C.