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 |
|
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 |
|
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 |
|
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 |
|
We can also combine multiple resultified function calls to find the names of all the books with at least one item in stock:
1 |
|
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 |
|
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!