In previous post I wrote about SuccessOrFailure and how it (usually) should not be used for exception handling. But the question remains: if I want to be good programmer, how should I handle errors if something as cool as SuccessOrFailure is “forbidden”? Fortunately, in SuccessOrFailure’s KEEP and corresponding discussion, Roman Elizarov explains how he sees exception handling. Is there anyone better to tell us how to handle exceptions in Kotlin other than Kotlin libraries team lead?

So how should I handle exceptions

In KEEP Roman Elizarov describes why SuccessOrFailure should not be used - he thinks that Kotlin provides many good alternatives.

What are the possibilities?

For the sake of example we have repository with function:

fun findUser(id: Int): User? {
        return User("")
    }

and we want to have wrapper function that uses this repo].

  1. Write good code that doesn’t have unexpected situations :sunglasses:

     fun doNothing() {}
    
  2. Throw exceptions

     fun getUser(id: Int): User {
         val user = repository.findUser(id)
         return user ?: throw RuntimeException("User not found")
     }
    
  3. Return Kotlin’s nullable type

     fun getUser(id: Int): User? { // note return type is nullable now
         return repository.findUser(id)
     }
    
  4. Return result class

     sealed class UserSearchResult
     data class UserFound(val user: User) : UserSearchResult()
     object UserNotFound : UserSearchResult()
     object DatabaseOffline : UserSearchResult()
    
     fun getUser(id: Int): UserSearchResult {
         val user = repository.findUser(id)
         return if (user == null) {
             UserNotFound
         } else {
             UserFound(user)
         }
     }
    
  5. Error-type/monad like Try, Either

     fun getUser(id: Int): Try<User> {
         val user = repository.findUser(id)
         return if (user == null) {
             Try.raise(RuntimeException("User not found"))
         } else {
             Try.just(user)
         }
     }
    

Wait a minute, but is that much different than exceptions in Java? It definitely is! Nullability in Kotlin works much better, data classes makes returned model easier to create and exceptions support in Kotlin is more flexible which encourages to not rely on them too much.

So which is the best?

As you probably know there is no such thing as checked exception in Kotlin. This means that if you use some method you might not know it throws exception and the whole thing might blow out, even though exception here is reasonable and you’d expect it to happen sometimes.

This is one of the drawbacks of exceptions and why functional community treats them as hidden information and side-effect. Return type of the function should give all info that function consumer might ever need.

So it looks like Kotlin wanted to disregard exceptions, right? Sure, there is try-catch for interoperability but in general - exceptions are bad! Right?

That’s what I thought. But apparently exceptions still have their place in Kotlin and Roman Elizarov paints clear distinction between two use cases:

  • errors handled locally
  • errors handled globally

Errors handled globally

Errors handled globally are “fatal” errors that we don’t know what to do with. You might call them “real” exceptions - exceptional, sometimes unpredictable situations which are not recoverable. They should blow up and they should be handled at higher level, probably several levels above exception source.

Handling might only mean logging this exception and printing its stacktrace. And that’s where stacktrace is handy - it can be analyzed by developers working to fix the issue (if fixable). Building stacktrace is expensive operation and here it pays off as it is really needed.

Errors handled locally

Obviously - contrary to previous case - these are exceptional situations that we expect and we know what to do with them. Our logic can recover from them. One might argue that they should be called “exceptions” at all.

Here handling is close to call site, often immediately. Building exception is costly and for such small scope it would be overkill. The simplest solution is: Kotlin nullable type.

If you think about it, this makes a lot of sense. Let’s suppose we want to fix fun getUser(id: Int): User and make return type more explicit about possible failure.

What is the difference between fun getUser(id: Int): User? and fun getUser(id: Int): Try<User> ? Essentially, it means the same - either you get back value, or something went wrong. And you need to handle both cases. Sure, Try gives you more functions, but nullability has language-level support. So in that simple case User? as a return type would play well.

“But I need to know what happened so I can make decision base on it” you might say.

That’s a valid point! And that’s where domain-specific result class (represented by sealed class) shines:

sealed class UserSearchResult
data class UserFound(val user: User) : UserSearchResult()
object UserNotFound : UserSearchResult()
object DatabaseOffline : UserSearchResult()

In Kotlin defining such classes is very simple - in Java this code would be much bigger.

In short

There are three cases

  • Unexpected situation that should blow up and we don’t know how to handle it locally - use exceptions
  • Expected situations that we handle locally:
    • No additional info needed - something was or wasn’t done - use nullable types
    • Additional info needed - there are several distinct outcomes - use domain-specific result class

But… functional style!

And what about functional style with Try/Either? It’s simple - you can do that if you want. Kotlin is designed to be reasonably flexible and while it is opinionated, functional programming is a big part of it.

So if application is designed in functional style and functional constructs are all around, then feel free to use Try. It would usually mean to add Arrow dependency, but for functional style you would probably use Arrow anyway.

BTW It’s a pleasure to read how Roman Elizarov discusses this issue - openly, politely and logically. If every team lead would be like that :heart: