The result pattern with Kotlin

At first, checked exceptions seemed like a novel solution to error handling in Java. Since then, developer’s abuse of the power entrusted upon them, has lead to a widespread disdain towards checked exceptions. Java 8 brought us streams and functional programming. Rather notably, checked exceptions were not part of these APIs. Similarly, Kotlin avoided implementing checked exceptions from the get go.

The main issues with checked exceptions in Java are:

  • Previous bad API design. You must catch errors even when they are impossible to occur
  • The compiler doesn’t trust us. We are forced to catch errors straight away
  • Handling of errors might not make sense straight away leading to rethrows
  • Sometimes, we cannot or don’t want to recover from an exception
  • Things get wordy. Try-Catch blocks for multiple error types can become large and cumbersome

The Kotlin documentation briefly describes reasons for avoiding checked exceptions in the language design.

Languages such as C++, Objective C, C#, Scala and most importantly Kotlin do not support checked exceptions. Instead, unchecked exceptions are used. By using unchecked exceptions, we take the metaphorical stabilisers off. Exceptions must be carefully managed through our code but we also get ultimate control of how, where and if we recover from errors.

In this post, I will talk about the Result pattern, what it could bring to the language and why you might want to consider using it in your own projects along with its checked exceptions.

A brief introduction to the result pattern

When we call a function in Kotlin, we know a few things: the function name, its parameters, optional parameters, default values and the return type. We do not however, get any information about the potential exceptions that could be thrown.

What does a function really return? When a function is successful, we return an object of some return type X. In the event of failure, we return some exception E. Without typed exceptions, the function signature only suggests the successful path is possible. We do not see any information about possible failures.
In a strongly typed language, the actual return type is better represented with a Pair of <X, E>. This pair can be called a Result. The left hand side the Success and the right hand side the Failure. This gives us a type with a Success OR a Failure.

For a more gentle introduction to the concept of Results being represented by a Pair, I recommend you read the Failure is not an Option - Functional Error Handling in Kotlin series.

Implementations of the result pattern

Kotlin has implemented its very own Result type that comes close to providing us with a Pair of <Success, Failure>. However, it stops shy of providing the error type.

Result4K offers us a production ready an implementation of Result that provides us with explicit Success and Failure.

So which of these Result types should you use in your project?
In my opinion, as a project grows, the number of custom Error types can grow somewhat out of control. Failure signatures become quite generic and we begin to lose the benefit of having typed errors (Sound somewhat familiar?). This does not mean that you should use the built in Kotlin Result type however. This Kotlin Result type is experimental and you are unable to return a Kotlin Result from a function without a compiler argument. This API could change in future so also comes with some risk.

Because Result4K is “production ready”, the rest of this article will be using Result4K exclusively.

The result pattern in action

The following examples can be found in the following GitHub repository.

A simple function

The following snippets show how simple it is to return a Success or Failure with Result4K. We can pass any type into a Success and Failure. Here, we are getting a greeting of type String else a Failure of Exception.

1
2
3
4
5
fun getSuccess(): Result<String, Exception> = Success("Hello")
fun getFailure(): Result<String, Exception> = Failure(Exception("Bang"))

fun getGreeting(shouldSucceed: Boolean) =
if (shouldSucceed) getSuccess() else getFailure()

Chaining computation using monads

Result4K provides us with functional methods to pipe execution. When we want to apply some transformation to a Result, we can use map() or flatMap(). Our transformations will only apply on a Success. Similarly, we can use mapFailure() to run a transform on a Failure.

1
2
3
getGreeting(shouldSucceed = true)
.map { "$it world!" }
.mapFailure { Exception("We failed with a new exception") }

Now that we have explored the basics, lets take a look at a more “real world” example.

The book shop

You are the owner of your very own book shop. Maybe one day you will be the next Jeff Bezos?
When running a bookshop, you must know what you have have in your store. A small bit of kotlin development later, we have the following:

  • a BookService that allows us to get the name of a book
  • a StockService that provides us with stock information about a book

When you look through the code below, you will notice that a new kotlin inline wrapper function resultFrom { }. This function allows us to wrap up our computation and business logic within a try/catch block and return a Result4K Result type. It is great for calling off to third party libraries or retrofitting the result pattern into your code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BookService {
fun getName(id: Int) = resultFrom {
NAMES[id] ?: throw Exception("No name found for $id")
}

companion object {
val NAMES = mapOf(
1 to "Effective Java",
2 to "Clean Code",
3 to "Quarkus Cookbook",
4 to "Kubernetes Best Practices",
5 to "Atomic Kotlin"
)
}
}

class StockService {
fun getStockCount(id: Int) = resultFrom {
STOCK.getOrDefault(id, 0)
}

fun getBooksWithStock(stockLimit: Int) = resultFrom {
STOCK.filter { it.value >= stockLimit }.keys.ifEmpty {
throw Exception("No books found with at least $stockLimit in stock")
}
}

companion object {
val STOCK = mapOf(
1 to 10,
2 to 25,
3 to 0,
4 to 7
)
}
}

Calling these resultified functions are no different to our earlier example.

We can calculate the number of characters in the name of a book:

1
2
bookService.getName(id)
.map { it.count() }

We can also combine multiple resultified function calls to find the names of all the books with at least one item in stock:

1
2
3
stockService.getBooksWithStock(1)
.map { ids -> ids.map { bookService.getName(it) } }
.map { "Found books with stock: [${it.allValues()}]" }

Combining many results

What happens when we have multiple results that we want to combine?
By using the map and flatMap functions, we lose context of our previously transformed result.
To apply a single transform over a number of results, we use zip.

1
2
3
4
zip(
bookService.getName(id),
stockService.getStockCount(id)
) { name, count -> "The book '$name' has a stock count of $count" }

Should you use the result pattern?

So, should you be using the result pattern in your project? Unfortunately, the answer is maybe.

The pattern is great if you want to move towards a more functional and “pure” way of doing things. It enables you to ensure success in your applications by giving you the tools to control the flow of happy and sad paths through your system.
Do you work with business critical software with many moving parts and external dependencies? The result pattern may well be for you.

Do you work on a tiny microservice without external dependencies? You might not gain a great deal by using the result pattern in your project. However, you can experiment with the pattern gradually if you wish to do so.

A few words of warning

As a project scope becomes larger, there is a chance that you will see yourself trying to manage a huge list of “failure reasons” (the types you use within your Failures). Make sure that this is not left uncontrolled.
You will also ultimately also find yourself wondering, what is a failure? Do we mean an external service failure? an internal one? Maybe we expect failures sometimes; is this actually a “success”? My thought, is that a failure is anything that is unexpected within a unit of execution. This could mean that one Result’s Failure is another Result’s Success.

Extra references

To read more about Kotlin and it’s current stance on Exceptions, head over to Roman Elizarov’s blog post on Kotlin and Exceptions. This post goes into much of the history of checked exceptions, what went wrong and why Kotlin has not implemented them.

In my next blog post I will write about how we can use concurrency alongside the result pattern, using Coroutines and Result4K. Stay tuned!