A take on functional error handling in Kotlin

Mattia Roccaforte
4 min readDec 24, 2019

--

Since the release of Kotlin 1.3, we’ve seen the introduction of the Result type, which is a union between a Success type and a Failure type. Its purpose is to encapsulate the result of an action, whether successful or not, allowing it to be processed at a later time. The reasoning behind this API addition, as well as concrete use cases, are discussed in the KEEP-127.

Together with Result, the standard library has seen the introduction of a bunch of new ancillary functions to work with the new type and one of those is runCatching:

The implementation of the function

As we can see, the primary purpose of runCatching is to concretely create a Result: it will execute a block of code and return its success or its failure encapsulated in a union type.

A second and probably more immediate use case for this simple function is functional exception handling. Kotlin has always encouraged writing code in a functional style and runCatching makes a perfect addition to substitute the “old” imperative try / catch construct.

Pardon the irony. I do like functional programming, I believe that when correctly used, it can really make code more readable and robust, even funnier to write down. However, in this particular case, there are some caveats that I will explain.

Ain’t gotta catch’em all

Exceptions are a powerful mean to express behavior, specifically what a function can’t do and why.

Methods should throw exceptions that explain what went wrong: was there some bad argument passed? Here’s an IllegalArgumentException. Some nullable variable was required non-null but it wasn’t? You have an IllegalStateException, and so on. They also should usually carry a message that describes the specific failure to better understand (and debug) the failure.

A fundamental concept to understand is that exceptions are not evil; yes, they may make your program crash and make some users angry, but it’s not really the exception’s fault — that would most likely be our fault for not properly handling it!

At this point then, many beginners (or slackers) would think that catching all the possible exceptions is a good thing to do: if I catch’em all, nothing can crash! You may be right, you will probably avoid some crashes, but crashing is not the only way for a program to behave badly: not responding to input for example, or giving poor feedback, make user experience just as bad.

Catching all errors means that you won’t know what is the specific exception type and you won’t be able to recover from the error accordingly.

Now let’s give a second look at the implementation of runCatching in the gist above. What does it do? It catches everything.

In this case, it goes even further: it catches all Throwables. For those not knowing, Throwable is everything that can go after a throw keyword; it has two descendants: Exceptions and Errors. We haven’t mentioned Errors so far; Errors usually represent something wrong that happened at a lower level than your business logic, something that can’t usually be recovered with a simple catch. For example: OutOfMemoryError, StackOverflowError, NuSuchMethodError: if any of these happens, you realize it’s time to stop and carefully think about what is going on in your code, you can’t just catch them and show a polite error message. Common practice in fact is to never catch Errors, that’s why we usually catch Exception subclasses only.

This is the main pitfall of using runCatching for exception handling: it leads programmers not to think about proper error recovery, it just eats every error and sort of hides it from you.

Obviously, if we are responsible programmers, we can still make use of the functional style of runCatching and also make a proper error handling, since after all the onFailure extension function gives us the chance to handle the Throwable:

Note the else clause of the when statement: if the error is not something that we expected and that we were ready to handle, rethrow it. Don’t let it be swallowed in silence, let the program crash, read the stacktrace and fix the code.

The coroutine breaker

Of all the side effects of using a catch-all way of handling errors, with Kotlin there’s one that in my opinion is very vicious and it’s my number one reason to avoid using runCatching unless I really know what’s being catched within it.

A suspending function guarantees that before resuming from the suspension point, it will check if the coroutine itself has been canceled. If it finds out that this is the case, it will acknowledge the cancellation by throwing a CancellationException, which is nothing more than a normal Exception with the only difference that it doesn’t make our program crash, because a CoroutineExceptionHandler catches it for us. The final result of this, is that the rest of the coroutine after the suspension point is no more executed.

Have a look at the following trivial example:

Think of an Android app: what happens if the user presses the back button that closes the current screen while it’s still loading the data? The back press calls the presenter’s onUserCanceled(), the coroutine scope cancels its children Jobs and we expect that the data loading is interrupted.
Instead, we will probably see some crash due to the fact that we are someway trying to access UI that is no longer on screen from within the View object. But... View’s methods are called after the suspension point, and my coroutine has surely checked for cancellation before resuming from there.
You can be sure it did, and it did throw its CancellationException as always… only for runCatching to swallow it. Since that exception has not been propagated, the coroutine machine doesn’t stop: the code after the suspension is executed potentially causing some fatal error. runCatching gets in the way of the coroutine’s inner mechanics and just breaks it.

That’s the importance of catching exceptions explicitly.

So, next time you find yourself using runCatching , remember that you should pay the necessary attention of what could happen inside its block: letting it swallow exceptions is fine only to the point that we are aware of all the possible exception that can be thrown, and/or we don’t consciously mind about handling each one of them in a specific way.

--

--