In this post, I'll be taking a look at code Exceptions - what they are, where they come from and how we can handle them. Examples will be in Kotlin.
Table of Contents
- Throwable Superclass
- Errors
- Exceptions
- Catching Exceptions
- Throwing Exceptions
- Centralised Error Handling
- Custom Exceptions
- Stack Trace
- Things to Consider
First off, let's get some context and look at where the Exception class comes from:
👑 Throwable Superclass
Exceptions are a subclass of the superclass Throwable.
For languages such as Kotlin that run on the JVM, the class Throwable
represents anything that is unusual or exceptional in your application.
Exceptions and Errors are the only direct subclasses of Throwable - however, many more subclasses stem from these two, and developers also have the opportunity to create their own, custom subclasses, too.
Errors V Exceptions - what's the difference?
In a nutshell:
Errors occur when something goes wrong with the Java Virtual Machine (JVM). They are not usually recoverable.
Exceptions occur when something unexpected happens in your application during runtime. Exceptions can be recovered from.
Padding the nutshell out a bit:
Errors
Errors prevent an application from completing its task, resulting in it terminating or needing to be restarted.
An Error can be caused by external factors such as system failures or resource depletion.
There are many error types, a few of the the common ones are:
Error Type | Example Errors |
---|---|
Class Loading Errors |
ClassNotFoundException , NoClassDefFoundException
|
Linking Errors |
NoSuchMethodError , IncompatibleClassError
|
Runtime Errors |
NullPointerException , ClassCastExcpetion
|
System Level Errors |
OutOfMemoryError , StackOverflowError
|
Compilation Errors | Syntax Issues, Type Mismatches |
Exceptions
Exceptions occur when something unexpected - but not entirely beyond imagination - has happened within the code.
Exceptions interrupt the expected flow of a program, but (unlike Errors) they can be caught and dealt with (‘handled’).
In handling exceptions, we have the opportunity to recover from them and keep our application running. It's up to the Developer to decide how we handle these Exceptions - a few common coding scenarios and possible handling ideas are listed below:
Exception | Handled by: |
---|---|
File not found | Create default file |
Invalid input | Request another input from user |
Network failure | Retry connection |
- Note that an uncaught exception will become a runtime Error - so we must make sure to catch them!
Runtime Exceptions deserve an honourable mention. They describe exceptions that occur within the JVM (during runtime). Runtime exceptions are extremely common, so much so that the compiler allows them to go uncaught and unspecified.
Some common types Runtime Exceptions that you might have already encountered:
NullPointerException
- when you try to perform operations on a null value
IllegalArgumentException
- when an illegal argument has been passed
IllegalStateException
- if an object has state that’s invalid
Catching Exceptions
In Kotlin, we can catch Exceptions by wrapping code in try-catch blocks. The exceptions we're looking to catch are not mistakes made by the engineer but plausible but plausible scenarios that shouldn’t happen, but could.
Some example scenarios where we might want to catch an exception:
- A user enters an invalid username/password combination
- A file that we are searching for is not found
- User attempts to retrieve data that should exist in a database but doesn't
- A user inputs zero as a divisor
To take this last example and put it into some code:
fun divider(a: Int, b: Int): Int {
return try {
a / b
} catch (e: Exception) {
e.printStackTrace() // print the full stack trace of the exception
0 // return a safe, default value
}
}
Throwing Exceptions
We don’t always have to catch an exception at the point of its creation - in fact, it is often better to let the exception bubble up through the call stack. We do this by throwing our exception.
Throwing an Exception initialises an object with dynamic storage duration called the Exception Object. It might help to think of this Exception Object as a hot potato. The first person to pull it from the oven isn't wearing gloves and the potato gets tossed along a line of people until it reaches someone who is wearing gloves.
A few reasons why throwing an Exception (rather than just catching in situ) is a good idea:
- DRY PRINCIPLES Avoids multiple try catch blocks everywhere, resulting in cleaner code that is easier to read and test
- CONTEXT There may be multiple scenarios in which the code triggering the exception is called. Each of these might benefit from a different, custom, response.
- RETRIES If you catch an exception too early, you loose the opportunity to retry the call from an API level
Here's our divider function example again, this time, however, the Exception is thrown and caught further up the call stack.
fun main() {
try {divider(2, 0)}
catch (e: Exception) {
e.printStackTrace()
0
}
}
fun divider(a: Int, b: Int): Int {
if (b == 0 || a == 0) {
throw Exception()
}
return a/b
}
Centralised Error Handling
In larger applications, all Exceptions can be managed in a single location, ensuring consistent handling and improved maintainability. This often happens on the very outer edge of the application, within the HTTP handler and is known as Centralised Error Handling.
Whether or not we choose to implement Centralised Error Handling in our application, a good rule of thumb when working with Exceptions is 'Throw early , catch late'.
Custom Exceptions
Clarity is King when it comes to coding. Custom Exceptions allow us to be really specific about what's going wrong in a given instance - making unexpected behaviours easier to understand. We can create Custom Exceptions by extending the Exception class (or one of its subclasses).
When creating a Custom Exception we can choose whether or not to pass an Exception message string as a parameter.
Using our divider function as an example again, here are some custom exception ideas:
No message:
class DivisionException() : ArithmeticException()
// remember we can create our Exception from any Exception subclasses
fun main() {
throw DivisionException()
}
versus with Message:
class DivisionExceptionWithMessage(message: String) : Exception(message)
fun main() {
throw DivisionExceptionWithMessage(“You can add any message you like here and the message can be different in every instance where it is thrown”)
}
Custom Exceptions can be particularly useful when we want to observe unexpected behaviour specific for business logic - where the standard exceptions available don't quite describe the problem as we would like.
Our divider function, now using our custom exception (with message):
fun divider(a: Int, b: Int): Int {
if (b == 0 || a == 0) {
throw DivisionExceptionWithMessage("you cannot divide by zero")
}
return a/b
}
fun main() {
try {divider(2, 0)}
catch (e: DivisionExceptionWithMessage) {
e.printStackTrace()
}
}
When we run the main function above, we get the following response in the IDE console:
Stack Trace
The above image is an example of a Stack Trace. The Stack Trace is a list of the method call hierarchy. It shows us what the problem is, where to find its origin and how it was called.
To break down this Stack Trace:
Line 1 - Exception type and message - nice and clear thanks to custom Exception.
Line 2 - Points to the divider function, where the Exception was thrown.
Line 3 - Shows the exact line where the divider function was called.
Line 4 - Indicates the entry point to the application and where the Stack Trace starts.
- Note that because we followed the 'Throw early, catch late' philosophy, we got a full stack trace and this helps us get the full picture of where and why our Exception was triggered.
Things to Consider
🔑 Security
We must carefully consider the information we disclose in custom Exception messages. Overly revealing messages will leave your application vulnerable to hackers and cyber criminals. If in doubt, an exception with no message is preferred.
Application speed
Throwing exceptions can slow down your application - something to bear in mind.
Real world Exception handling
In the examples in this post, we handle our Exceptions by printing the full stack trace to the console and/or returning a safe, default value. Whilst this approach is fine for small applications and exercises, a better approach for real applications would be one/all of the following:
- Log the message to somewhere like Datadog where we can monitor and review our application
- If the Exception occurred in a Front End application we could return an error response to the user, requiring them to submit another set of numbers
- If the Exception occurs in an API, the Exception could be returned as a Http Status code - eg 400 Bad Request
Checked / Unchecked Exceptions
If you have a Java background, you may have heard of checked and unchecked exceptions. In Kotlin, all exceptions are unchecked — which means it's up to us to decide what to handle 😎.