Exceptions in Kotlin

Just like several other languages, Kotlin includes exceptions to indicate something went wrong in your application. Here we saw the NumberFormatException when we tried to convert a String that had stored a Double into an Int.

Another well-known exception is the NullPointerException which occurs when a variable is accessed and it’s not pointing to any object and refers to nothing or null.

However, exceptions are not used only by the system. You can use your own to indicate potential issues in your program.

📌 This is not a Kotlin feature, though. Several languages allow you to manually point out that an exception has occurred.

Exceptions can be useful to indicate something went wrong in the program and that it must be handled before the execution continues. A very common one is the IllegalArgumentException which indicates you have provided an input that has been considered illegal.

Take the obtainQuest function, for example. We should probably make sure the player level is not a negative number. We can do that with a conditional check:

const val CHAR_NAME = "Frodo"
var charLevel = 1

fun main() {
    println("$CHAR_NAME departs on his journey.. ")
    println("What level is $CHAR_NAME?")
    val charLevelInput = readLine()!!
    charLevel = if (charLevelInput.matches("""\d+""".toRegex())) {
        charLevelInput.toInt()
    } else {
        throw IllegalArgumentException("The player's level needs to be at least 1")
    }
    println("$CHAR_NAME's level is $charLevel.")

And if you enter -1 as the character’s level you will get the exception warning:

Please note that when you throw an IllegalArgumentException you don’t need to pass a message as an argument:

However, if you do provide a message to the IllegalArgumentException it’s easy to know exactly why the program failed.

Handling Exceptions

So, this is great, you added a message that will make clear what caused the exception, but the program crashed and was terminated anyways. Can we avoid that?

We can use a Try/Catch statement to handle these cases. The code would look like this:

private fun checkQuestLog() {
    val message: String = try {
        val quest: String? = obtainQuest(charLevel)

        quest?.replace("Sauron", "him who we do not name.")
            ?.let { restrictedQuest ->
                """
            $CHAR_NAME checks the quest log. It reads:
            "$restrictedQuest"
            """
            } ?: "There are no quests for you at the moment"
    } catch (e: Exception) {
        "$CHAR_NAME can't read what's on the quest log."
    }
    println(message)
}

When you add a try/catch statement, what you are saying to the compiler is that the block of code within the try part of the statement is the code you would like to try. So, basically, in the case above, you are trying to obtain the quest based on the charLevel variable. If no exception occurs (the level input is > 0), the code is executed and the catch part is ignored. The code continues.

val quest: String? = obtainQuest(charLevel)

        quest?.replace("Sauron", "him who we do not name.")
            ?.let { restrictedQuest ->
                """
            $CHAR_NAME checks the quest log. It reads:
            "$restrictedQuest"
            """
            } ?: "There are no quests for you at the moment"
    }

In the catch block you define what will happen if the code in the try block causes an exception.

catch (e: Exception) {
        "$CHAR_NAME can't read what's on the quest log."
    }

The exception in this case (differently from the first example at the beginning of the post) is determined during the obtainQuest function. Note the require(charLevel > 0) which is self-explanatory. It requires the charLevel to be greater than 0 (zero):

private fun obtainQuest(
    charLevel: Int,
    hasTheRing: Boolean = true,
    hasFoundAragorn: Boolean = true
): String? {
    require(charLevel > 0) {
        "The player's level must be at least 1."
    }

    return when (charLevel) {
        1 -> "Meet Samwise Gamgee and leave the Shire"
        in 2..4 -> {
            val canGoToRivendell = hasFoundAragorn && hasTheRing

            if (canGoToRivendell) {
                "Follow Aragorn to Rivendell"
            } else {
                "Go back to the The Prancing Pony"
            }
        }
        5 -> "Help Aragorn defeat the Nazgûl"
        6 -> "Defeat Laracna"
        7 -> "Head to Mount Doom"
        8 -> "Destroy the ring in the fire of Mount Doom and defeat Sauron"
        else -> null
    }

So, when the game asks you for the character level and your input is 0 (zero) the catch part of the code is executed and the program does not crash, it continues:

This technique can be useful if you want to execute potentially dangerous code to calculate a value and have a fallback option that you can use in case something goes wrong. It ensures you will handle the exception. If you delete the string in the catch block, the compiler would complain that it does not provide a string value to assign to the message.

Preconditions

We cannot expect users to behave like we intend them to. Unexpected values will happen and cause the app/program to behave in unexpected ways and we will spend a lot of time making sure our code is prepared to handle these behaviours. That said, some exceptions are pretty common, so, to make our lives a bit easier when you need to validate input and debug in order to avoid certain commonly known issues, Kotlin provides some very convenient functions as part of its standard library.

They are called Precondition Functions since they allow you to define preconditions that must be true before a piece of code is executed. One of these functions is require which checks if a value is null. If it is, require returns an IllegalArgumentException and if it’s not the function returns the value.

So, if we use the require function in the example above it would look like this:

private fun obtainQuest(
    charLevel: Int,
    hasTheRing: Boolean = true,
    hasFoundAragorn: Boolean = true
): String? {
    require(charLevel > 0) {
        "The player's level must be at least 1."
    }

    return when (charLevel) {
        1 -> "Meet Samwise Gamgee and leave the Shire" 
...

Using this function reduces the amount of code needed for the IllegalArgumentExcepssion. The require takes two arguments: one is a boolean that checks if the expression is true or false (charLevel > 0) and the second one is the error message to be included in the thrown exception.

You can find below the precondition functions included in Kotlin:

FunctionWhat it does
checkThrows an IllegalStateException if an argument is false
checkNotNullThrows an IllegalStateException if an argument is null. If it’s not it returns the non-null value
requireThrows an IllegalArgumentException if an argument is false
requireNotNullThrows an IllegalArgumentException if an argument is false. If it’s not it returns the non-null value
errorThrows an IllegalArgumentException with a provided message if the argument is null. If it’s not it returns the non-null value
assertThrows an AssertionError if an argument is false and the assertion compiler flag is enabled.

At the end of the day, these are one more way for you to handle null safety. The result would be pretty similar to using the non-null assertion operator but instead of throwing a super generic NullPointerException you can provide more information about what exactly went wrong.

Preconditions are a very convenient way to ensure your application is running smoothly – and if it is not, failing as soon as possible to help debug the issue. They can help you easily throw the most commonly used exception types and save time and also some boilerplate code.

Published by Rosie M

Android bug creator and full-time nerd. Joined Automattic in 2017. Passionate about music, books, games, photography and Doctor Who. Open-source enthusiast, remote-work advocate and Globetrotter.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: