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

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.

Image description

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:

Image description

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 😎.