Authors Top

If you have a few years of experience with the Kotlin language and server-side development, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Overview

Since version 7, Java has supported catching multiple exceptions in one catch block, for example:

// Java code
try {
    // ...
} catch (Exception1 | Excpetion2 ex) {
    // Perform some common operations with ex
}

However, Kotlin hasn’t supported this feature until the current version (1.7.0). So, in this tutorial, we’ll explore how to implement the “multi-catch” feature in Kotlin.

2. Creating an Example

To demonstrate the solutions in this tutorial, let’s first create an example function to generate exceptions.

2.1. KeyService and Exceptions

Let’s say we need to create a KeyService to store a string key. Further, we have a few requirements for saving a “Key“:

  • The key’s length must be six.
  • Only digits are allowed to be used in a key.
  • The same key can be stored only once.

Therefore, let’s create four exceptions:

class KeyTooLongException(message: String = "Key-length must be six.") : Exception(message)
class KeyTooShortException(message: String = "Key-length must be six.") : Exception(message)
class InvalidKeyException(message: String = "Key should only contain digits.") : Exception(message)
class KeyAlreadyExistsException(message: String = "Key exists already.") : Exception(message)

Next, let’s create KeyService to check and store a given key:

object KeyService {
    private val keyStore = mutableSetOf<String>()
    fun clearStore() = keyStore.clear()
    fun saveSixDigitsKey(digits: String) {
        when {
            digits.length < 6 -> throw KeyTooShortException()
            digits.length > 6 -> throw KeyTooLongException()
            digits.matches(Regex("""\d{6}""")).not() -> throw InvalidKeyException()
            digits in keyStore -> throw KeyAlreadyExistsException()
            else -> keyStore += digits
        }
    }
}

As the code above shows, we validate and save the input key in a when{} block.

For simplicity, we make the KeyService an object. Further, after the validation, we store the key in a Set.

2.2. The SaveKeyResult Enum

As we’ll address different solutions to handle multiple exceptions in the save() function, let’s create an enum to represent the result of the saving function:

enum class SaveKeyResult {
    SUCCESS, FAILED, SKIPPED_EXISTED_KEY
}

The enum above has three constants:

  • SUCCESS – the given key’s valid and successfully stored
  • FAILED – when KeyTooShortException, KeyTooLongException, or InvalidKeyException is thrown
  • SKIPPED_EXISTED_KEY – the key’s valid, but already exists in the store. In other words, we want this result when KeyAlreadyExistsException is thrown

So next, we’ll implement different approaches of the save() function to call KeyService.saveSixDigitsKey(). Of course, our save() functions will handle exceptions and return the corresponding SaveKeyResult. To distinguish each save() function, we’ll add an index suffix after the function name for each solution, such as save1(), save2(), and so on.

For simplicity, we’ll use unit test assertions to verify if our solutions work as expected. Further, we’ll use the same input data to test each approach. Therefore, we need to clear the keystore set before starting each test:

@BeforeEach
fun cleanup() {
    KeyService.clearStore()
}

Next, let’s see how to catch multiple exceptions in Kotlin.

3. Multiple catch Blocks

The most straightforward solution would be to catch each exception and process the required operations, such as returning the SaveKeyResult instance accordingly:

fun save1(theKey: String): SaveKeyResult {
    return try {
        KeyService.saveSixDigitsKey(theKey)
        SUCCESS
    } catch (ex: KeyTooShortException) {
        FAILED
    } catch (ex: KeyTooLongException) {
        FAILED
    } catch (ex: InvalidKeyException) {
        FAILED
    } catch (ex: KeyAlreadyExistsException) {
        SKIPPED_EXISTED_KEY
    }

The code above is pretty easy to understand. Next, let’s test it with different kinds of inputs:

assertEquals(FAILED, save1("42"))
assertEquals(FAILED, save1("1234567"))
assertEquals(FAILED, save1("kotlin"))
assertEquals(SUCCESS, save1("123456"))
assertEquals(SKIPPED_EXISTED_KEY, save1("123456"))

As the assertions above show, we’ve covered all cases of save1()‘s execution. If we run the test, it passes. So, this solution works.

This approach is pretty straightforward. However, the disadvantage is obvious too. We have duplicates in multiple catch blocks, and we need to create a catch block for each exception case.

Next, let’s see if we can make some improvements on save1().

4. Using when in the catch Block

One idea to reduce the duplicated code is to have only one catch block to catch Exception. Then, in the catch block, we use when{} to check the exception type and perform the required operation:

fun save2(theKey: String): SaveKeyResult {
    return try {
        KeyService.saveSixDigitsKey(theKey)
        SUCCESS
    } catch (ex: Exception) {
        when (ex) {
            is KeyTooLongException,
            is KeyTooShortException,
            is InvalidKeyException -> FAILED
            is KeyAlreadyExistsException -> SKIPPED_EXISTED_KEY
            else -> throw ex
        }
    }
}

As the code above shows, we catch the general Exception type and check the concrete exception type in the when{} block to simulate catching multiple exceptions.

Next, let’s test the save2() function using the same test inputs:

assertEquals(FAILED, save2("42"))
assertEquals(FAILED, save2("1234567"))
assertEquals(FAILED, save2("kotlin"))
assertEquals(SUCCESS, save2("123456"))
assertEquals(SKIPPED_EXISTED_KEY, save2("123456"))

Again, the test passes if we execute it.

5. Creating the multiCatch() Function

Kotlin has a nice feature: extension functions. So next, let’s see if we can solve the problem using extension functions.

First, let’s look at a regular trycatch structure:

try {
    DO_SOMETHING
} catch (ex: Exception) {
    ...
}

We can consider the DO_SOMETHING block as a function that takes no argument and return some type (R): () -> R. Then, we can extend the () ->R function type to handle multiple exceptions:

inline fun <R> (() -> R).multiCatch(vararg exceptions: KClass<out Throwable>, thenDo: () -> R): R {
    return try {
        this()
    } catch (ex: Exception) {
        if (ex::class in exceptions) thenDo() else throw ex
    }
}

Here, the multiCatch() function has two arguments. The first one is a vararg, containing the types of “multiple exceptions.” A function will be executed if any exception in the defined exceptions occurs. This function is the second argument: thenDo().

As we can see in the code above, we wrap the try-catch block in the multiCatch() function. Also, it’s worth mentioning that we’ve declared the multiCatch() function as an inline function to gain better performance.

Next, let’s see how to use the multiCatch() function in save3():

fun save3(theKey: String): SaveKeyResult {
    return try {
        {
            KeyService.saveSixDigitsKey(theKey)
            SUCCESS
        }.multiCatch(
            KeyTooShortException::class,
            KeyTooLongException::class,
            InvalidKeyException::class
        ) { FAILED }
    } catch (ex: KeyAlreadyExistsException) {
        SKIPPED_EXISTED_KEY
    }
}

As we’ve seen, we wrap KeyService.saveSixDigitsKey(theKey) and SUCCESS in { … } so that it becomes a () -> SaveKeyResult function. Then, we can call the predefined multiCatch() function. The first argument is the three exception types, and the second argument is pretty simple: a function returning FAILED.

We still need to handle other exception types apart from the three exceptions. Therefore, we have an outer try-catch block to handle the KeyAlreadyExistsException exception.

Next, let’s test if save3() works as expected:

assertEquals(FAILED, save3("42"))
assertEquals(FAILED, save3("1234567"))
assertEquals(FAILED, save3("kotlin"))
assertEquals(SUCCESS, save3("123456"))
assertEquals(SKIPPED_EXISTED_KEY, save3("123456"))

The test passes if we give it a run.

This solution defines a meaningful multiCatch() function, so it’s easier to read. However, the disadvantages are clear too. First, the multiCatch() function returns an R object instead of this (the ()->R function). Therefore, it can handle only one set of “multiCatchrequired” exceptions. Another inconvenience is like what we saw in the example – if we need to catch other exception types, we still need an outer try-catch structure.

Next, let’s see if we can keep the readability and solve the inconveniences save3() has.

6. Extending the Result Class

Kotlin has introduced the Result<T> class to encapsulate the result of some processing. In this section, we’ll implement catching multiple exceptions through extending the Result class.

6.1. Introduction to runCatching() and Result<T>

When we perform some operations, we can wrap the operations with the standard runCatching() function to get a Result<T> object as a result, for example:

runCatching {
    KeyService.saveSixDigitsKey(theKey)
    SUCCESS
}

The code above will return a Result<SaveKeyResult> object. If the saveSixDigitsKey() function is successfully executed, the Result object’s isSuccess attribute is true, and the result contains the value SUCCESS. Kotlin provides several ways to get the value wrapped in the Result object, for example: getOrThrow(), getOrDefault(), and getOrElse().

Otherwise, if an exception is thrown during the execution, the Result object’s isFailure attribute will be set to true, and the result object contains the exception.

In a failure case, the Result<T> class allows us to transform the exception into another object as the result’s value by calling the recoverCatching() function:

inline fun <R, T : R> Result<T>.recoverCatching(
    transform: (exception: Throwable) -> R
): Result<R>

So next, let’s see how these help us to handle multiple exceptions.

6.2. Extending the Result<T> Class

Now that we understand how recoverCatching() and Result<T> work, we can create an extension to the Result<T> class to handle multiple exceptions.

Next, let’s take a look at the implementations and then understand how it works:

inline fun <R, T : R> Result<T>.onException(
    vararg exceptions: KClass<out Throwable>,
    transform: (exception: Throwable) -> T
) = recoverCatching { ex ->
    if (ex::class in exceptions) {
        transform(ex)
    } else throw ex
}

As we can see, we’ve created an extension onException() to the Result class. The onException() function has two arguments. The first is a vararg, a group of exception types. The second argument is the transform() function.

The onException() function is based on recoverCatching(). Therefore, only if the result’s isFailure is true, our exception handling is performed. Moreover, the transform() function defines what we want to do if certain exceptions are thrown.

So next, let’s create save4() using the declared onException() functions:

fun save4(theKey: String): SaveKeyResult {
    return runCatching {
        KeyService.saveSixDigitsKey(theKey)
        SUCCESS
    }.onException(
        KeyTooShortException::class,
        KeyTooLongException::class,
        InvalidKeyException::class
    ) {
        FAILED
    }.onException(KeyAlreadyExistsException::class) {
        SKIPPED_EXISTED_KEY
    }.getOrThrow()
}

This time, as we can see, since we’ve used runCatching(), we don’t have any try-catch structure anymore. Further, if we need to handle multiple “multiCatch” groups, we can simply add more .onException{ … } blocks. 

Finally, let’s test if our save4() works as expected:

assertEquals(FAILED, save4("1234567"))
assertEquals(FAILED, save4("42"))
assertEquals(FAILED, save4("kotlin"))
assertEquals(SUCCESS, save4("123456"))
assertEquals(SKIPPED_EXISTED_KEY, save4("123456"))

When we run the test, it passes.

7. Conclusion

In this article, we’ve learned how to catch multiple exceptions in Kotlin.

As always, the full source code used in the article can be found over on GitHub.

Authors Bottom

If you have a few years of experience with the Kotlin language and server-side development, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!