Go-inspired Errors in Kotlin

Categories: Programming

Exceptions suck

Error handling and reporting is something that has always bothered me. If you use a checked exception model, your code rapidly becomes very verbose and your interfaces very brittle, which can often push programmers toward very lazy exception handling. On the other hand, if you use an unchecked exception model, you are completely dependent on the developers of the code you call documenting every exception that can be thrown so you know what errors can occur (unless you want to go spelunking through their code anyway). Further, unchecked models allow unsafe code to look completely fine since you do not have to acknowledge anywhere in it that you’re ignoring exceptions, which, while a valid approach in some circumstances, is generally not what you want.

Idiomatic Go errors

In my opinion, the best approach I’ve seen is treating errors as return values, which idiomatic Go does a pretty good job of doing. As a convention, any function that can experience an error will return an error value when it experiences a fault. Since Go supports multiple returns from each function, this means you regularly see this sort of construct in Go code:

desiredValue, err := doSomething(someArgument)
if err != nil {
	// handle error
}

This has the upside of making your error handling quite explicit in your code. You’re still at the mercy of the documentation to know what values err may take, but at least your code will always know when an error occurred without crashing the program even if lacking documentation prevents you from fully enumerating and handling all the error states.

That said, this also has the upside of allowing you to ignore error states you don’t care about by using a construct like this:

desiredValue, err := doSomething(someArgument)

if err == ValueNotFound{
	// show a 404
} else if err != nil {
	// log and show a 500
}

with however much specificity as you wish. If you want to ignore the error, you can just ignore the value of the error that is returned and carry on with your life. However, if you choose to do this, the fact that you are doing so will be clearly evidenced in your code, making it more understandable in the future. That said, if a function purely has side-effects and no returns, you can still silently ignore errors without leaving any evidence of doing so, but, in my opinion, these sorts of functions should make up a small enough portion of your critical code-paths that it’s not worth the headaches associated with using Java-style exceptions to avoid them.

Limitations of the Go error system

Go’s error handling model seems very nice at first blush, but the longer you work with it, the more warts you will start noticing. The first such wart most people notice is the repetitiveness of the if err != nil blocks, which can sometimes end up comprising the bulk of the code in a function. Granted, this isn’t necessarily a huge flaw, but it does result in more bloated code, which , in my experience, can be a barrier to understanding complex logic. Secondly, while it is arguably a poor practise to use errors to control program flow, realistically it is going to be necessary in all but the simplest of applications, if for no other reason than to decide what error message to show your user. There are ways to deal with these issues in Go, but, in my opinion, as a result of the limitations of the language itself, they all end up pretty inelegant.

Enter Kotlin

Kotlin has several interesting language features that are well suited to implementing a cleaner version of the Go error system. However, before we get into those, we have to start with how we’re going to get around Kotlin’s lack of multiple return values. It is possible to use destructuring to affect a multi-return, but the Kotlin destructuring system has always felt like a nasty hack to me (all that .componentX()…). Instead, I’m going to use Kotlin’s implementation of union types - sealed classes. With a sealed class we can make a single return value that is either a success value, or expressed that some kind of error happened. If we wanted to copy Go’s built in error type, we’d end up doing something like this:

interface GoError {
	val error: String
}

sealed class RetrieveProfileResult {
	data class Success(
        val name: String, 
        val birthday: Date
    ) : RetrieveProfileResult()
	data class Error(
        val error: String
    ) : RetrieveProfileResult(), GoError
}

However, since we are using the much more powerful Kotlin type system, we can easily be much more expressive with our error values without running into the limitations of the Go type system:

sealed class RetrieveProfileResult {
	data class Success(
        val name: String, 
        val birthday: Date
    ) : RetrieveProfileResult()
    object ProfileNotFound : RetrieveProfileResult()
    object CommunicationError : RetrieveProfileResult()
}

In my opinion, this approach is significantly better than the Go method since it makes the error types self-documenting as well as allowing the compiler to enforce that the programmer at least acknowledge that they’re choosing to ignore result values if they want to get a success value. Further, this approach works beautifully with exhaustive when expressions in order to provide a highly legible error handling approach. For example, if we are handling an http request that requires a user’s birthday, we can do something like this:

val birthday = when(val res = retrieveResult()) {
    is RetrieveProfileResult.Success -> {
        res.birthday
    }
    RetrieveProfileResult.ProfileNotFound -> {
        request.httpStatus = 404
        return
    }
    RetrieveProfileResult.CommunicationError -> {
        request.httpStatus = 500
        return
    }
}

Even better, imagine that a privacy feature is added later that makes this call also possibly return a PermissionDenied error type. Well, now the above code will fail to compile until we account for that error in our code, making the compiler now a partner in making sure we actually are exhaustive in our error handling when we are trying to be.

The when construct also gives us the power to easily combine finely-grained lower level errors into broader errors when it makes sense for us to do so, like so:

val exhaustiveResult = when(val res = doSubOperation()) {
	is SubOperationResult.ReadSVG -> {
		res.svgValue
	}
	is SubOperationResult.ReadPNG,
	is SubOperationResult.ReadJPG,
	is SubOperationResult.ReadWEBP -> {
		return OperationResult.UnsupportedFileFormat
	}
	SubOperationResult.FileNotFound,
	SubOperationResult.PermissionError -> {
		return OperationResult.CannotReadFile
	}
	SubOperationResult.FileFormatIncorrect,
	SubOperationResult.LastModifiedInFuture -> {
		return OperationResult.FileCorrupted
	}
}

Note that I just changed my result type in the above without telling you anything about this new operation, and it’s still easily understood without even ever seeing that type definition. This is also showing the other nice benefit of sealed return classes - we can have multiple ‘success’ returns if a function can have different success states that we may need to differentiate, as in this case where the code is clearly doing something that only valid with SVGs.

So far I’ve been focusing on exhaustive error handling, but what if we want to choose to ignore some errors? Well, we still have to take them into account somehow, as we should - they’re errors, something went wrong. However, sometimes we don’t really care exactly what went wrong, just that it did, and in those cases we can simply add an else block to ignore the nuances of errors we don’t care about:

val partiallyExhaustiveResult = when(val res = doSubOperation()) {
	is SubOperationResult.ReadSVG -> {
		res.svgValue
	}
	is SubOperationResult.ReadPNG,
	is SubOperationResult.ReadJPG,
	is SubOperationResult.ReadWEBP -> {
		return OperationResult.UnsupportedFileFormat
	}
	else -> {
		return OperationResult.FileError(cause = res)
	}
}

And of course if we so choose, we can completely ignore the error results:

val result = doSubOperation()

Now in this circumstance, we can’t use the success result data unless we force cast the result to a success type (which we probably shouldn’t do), but, in my opinion, this is desirable behaviour. If we want to use the result, we should have to do some kind of error handling, but if we don’t really care about the result (eg if its just doing something like logging a bit of non-critical data), we don’t have to. When we’re actually doing the error handling we can either have compiler-enforced exhaustiveness like checked exceptions if we want or relax those requirements a bit using else branches if we don’t. Our error handling code is easily understood, our functions are self-documenting of their error states, wrong code will look wrong, and we can lean on the compiler/our IDE to help reduce some common types of human error - an absolute win in my books.