Exceptions, Result Types and Kotlin

James
7 min readJan 27, 2021

Kotlin’s type system gives plenty of advantages over Java. The default to have types non-nullable, and adding nullability deliberately and optionally is a huge boon, made even better by the compilers ability to predict potential null reference errors. Something neither language does well (in my opinion), however, is handle error conditions.

Java’s checked exceptions are one of the most complained about features I’ve ever encountered, and with good reason. Having each method list every possible way in which it can go wrong, and having the caller have to directly deal with that is an exercise in boredom. I’ve never used another language that forces exception handling in this way — it’s painful, and it leads to this style of boilerplate everywhere:

Developers being developers, that todo never gets done. With all the good will that Java’s exceptions offer in forcing error handling, they cause many errors to simply be silenced.

Kotlin took the opposite approach, exceptions are unchecked. A method does not have to declare all the exceptions it may throw. You can try/catch exceptions, but if you don’t catch one and it is thrown it will keep going up your call stack until it’s either caught or the program exits with an error code. Most would say this is also bad, you now have no way of knowing if the whole program is going to fail due to something unknown.

I believe part of the issue is that more than on concept is often handled by exceptions, which makes them unwieldy (If we’re coding by principle, using Exceptions for two different jobs violates the single responsibility principle). The first use of exceptions is to convey that something external went wrong. E.g. an IOException occurring when someone accidentally unplugs a network cable while you’re reading from a database. The second common use is to denote that an attempted operation failed e.g. a user’s password was incorrect. There is a third, often less common, usage that suggests that an assumption of the programmer is incorrect (e.g. an error when attempting to divide by zero, when divide has been defined for Double but in reality that it is undefined for 0)

To untangle these, I’m going to draw from two other languages that, in my opinion, handle things better: Go and Rust. If you don’t know either of these that’s okay, I’m just going to take ideas they use and you will recognise if you do.

The first case — something external has gone wrong — I think is a good use of Exceptions. It’s in the name, something exceptional has happened. There’s no way to recover from this, and no way to predict it. I also think that allowing this sort of error to unwind the call stack is good. Either at the top level of your program you can have a generic handler that restarts everything; or, in the world of containers and serverless, you simply allow the program to crash and your orchestration software will log the error and restart your service. This is comparable to Go’s panic and Rust’s panic! — your application is in a completely unrecoverable state and should be acting as such.

The second use, denoting a failing operation, is not something that should be handled by exceptions. Using the example of a user logging in, a wrong password is a valid state for your system to be in. It isn’t exceptional, it will happen a lot. If not with exceptions, then how do we tell the caller that the login failed? With the return type of your method.

Return types, because they are demonstrated with simple code like getters or simple mathematical functions, often get overlooked in the amount they can express. They’re often looked at as the result of a calculation, and might be set to null if it fails, but you can do a lot more with them.

In Go, it is normal to return 2 values from a function. A result, and an error code. It is up to the caller to check if the error is set and handle it appropriately. This is separate to the panic function, this error code tells the caller what kind of result they’ve got. E.g. for a login method it might be nil on a success, but it might be set to 1 if the user doesn’t exist and 2 if the user exists but the account has been suspended. Instantly you have a lot more information that is actually useful, without having the potential for an exception to break your whole application.

Rust does something similar, however in rust it is normal to use the Result type. This can denote a success, with a value, or an error with a value. It is similar to Kotlin’s sealed classes. We’re going to replicate something like this, but also mix in some inspiration from Go’s error codes to make something I think is a much more powerful way to handle these results. We’re going to make some fairly heavy use of Kotlin’s generics to do so, but the end result is very usable.

First we’re going to define our Outcome class as a sealed class (we’re calling it Outcome rather than Result because Kotlin internally has a Result type, but that shouldn’t be used for return types). Our outcome is going to have two options: Ok or Err (again, names chosen to avoid conflicts with Kotlin’s own Error type). Technically, this technique would work with just the constructors defined, but I’ve added a helper method too — it’s defined as abstract methods in the Outcome class so that it’s always available to us. While the generics may look convoluted, often you won’t need to use them, as Kotlin’s type system is good enough to infer them most of the time. The class looks like this:

We have our outcome type, and it can be an error or a success. I’ve specified that error codes will always be in the form of an Enum — I find this helps readability, and that errors can have an extra object attached. That object is of Any? type because trying to make it generic would convolute this solution beyond readability.

Let’s have a look at using this in the real world In a real world project we’d probably define an interface first, and then implement it. Sticking with the user login example, our interface might look like this:

This might feel reminiscent of Java’s listing all of the possible exceptions, but it is useful here. These are all valid results of the method, they are not exceptional. We can still get IOException or other unexpected errors too.

Notice that the enum is part of the public interface — our caller needs to know how this will return. Also notice that we can have a different enum per method, making our code a lot more descriptive about what can happen.

A quick implementation might look like this: we have 2 users, bob and dave. bob’s password is “password”, while dave’s account has been deactivated. (a real implementation may call to a database or external service)

Fairly simple. Easy to read. And the use of the enum and Ok/Err make the code very descriptive, so we need fewer comments.

What about calling the code? If you look back at the Outcome definition you’ll see a unaryPlus operator. I’ve used + because it’s quick and easy to type. Rust uses unwrap() . It is a simple but effective method — if you call it on an Ok result, it returns the inner result (in this case, the instance of User ), but if you call it on an Err, you get an exception — we’re back to an unhandled panic. This gives us convenience for quick tests, or code that we know can’t fail, without having to wrap with a try/empty catch block, but we get an error if the programmer forgot to handle a condition. An example might look like this:

We have a very descriptive call and error handling here. We don’t have the issue of unexpected throws, unless they’re IO errors. As a bonus, the error handling goes before the result use, which I believe is better than nesting the success conditions into the try block.

Of course, you can take this a step further and have each method return its own sealed class of states, or make all states, including OK, part of the enum. I find having a general Outcome class like this a good compromise between usability and safety — add too many steps in (e.g. sealed return type per method) and developers will start to skip them.

What about the third use of Exceptions? The IllegalArgumentException ? In Kotlin you have two options:

  1. The require method. Require simply states something that must be true, and optionally a lambda to build an error message if the requirement is not met. If using this it is good practice to put them all at the start of a method. This will throw an IllegalArgumentException if not true
  2. Building types that do not allow the wrong values

Personally, I prefer to construct types that do not allow for incorrect values, so that the assumptions of the variable are built into the type. For example, if we were building the divide(a, b) method to divide A by B, There are two examples:

As you can see, the latter case will error the second you try to construct the variables, while the former only when they are used. The second is more work, but you know instantly if the value doesn’t fit in the container you want to use. Other languages have more interesting ways of achieving this, but I have yet to successfully apply them to Kotlin without losing a large amount of the safety the compiler gives you.

Either way, you still get an exception if used incorrectly. The best way to prevent these errors is unit tests. Your calling functions should never be providing the wrong arguments for a function.

I hope this has been useful. I am a big fan of taking ideas from other languages and applying them to the one I’m working in, even if they are not the normal mode of working for the latter.

--

--