Skip to content

michaelbull/kotlin-result

Repository files navigation

kotlin-result

Maven Central CI License

badge badge badge badge badge badge badge badge badge badge badge badge badge badge

A multiplatform Result monad for modelling success or failure operations, providing all three tiers of Kotlin/Native target support.

Installation

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
}

Introduction

In functional programming, the result Result type is a monadic type holding a returned value or an error.

To indicate an operation that succeeded, return an Ok(value) with the successful value. If it failed, return an Err(error) with the error that caused the failure.

This helps to define a clear happy/unhappy path of execution that is commonly referred to as Railway Oriented Programming, whereby the happy and unhappy paths are represented as separate railways.

Getting Started

Below is a simple example of how you may use the Result type to model a function that may fail.

fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
    return if (user.rank >= command.minimumRank) {
        Ok(command)
    } else {
        Err(CommandError.InsufficientRank(command.name))
    }
}

When interacting with code outside of your control that may throw exceptions, wrap the call with runCatching to capture its execution as a Result<T, Throwable>:

val result: Result<Customer, Throwable> = runCatching {
    customerDb.findById(id = 50) // could throw SQLException or similar
}

Nullable types, such as the find method in the example below, can be converted to a Result using the toResultOr extension function.

val result: Result<Customer, String> = customers
    .find { it.id == id } // returns Customer?
    .toResultOr { "No customer found" }

Transforming Results

Both success and failure results can be transformed within a stage of the railway track. The example below demonstrates how to transform an internal program error UnlockError into the exposed client error IncorrectPassword.

val result: Result<Treasure, UnlockResponse> =
    unlockVault("my-password") // returns Result<Treasure, UnlockError>
        .mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword

Chaining

Results can be chained to produce a "happy path" of execution. For example, the happy path for a user entering commands into an administrative console would consist of: the command being tokenized, the command being registered, the user having sufficient privileges, and the command executing the associated action. The example below uses the checkPrivileges function we defined earlier.

tokenize(command.toLowerCase())
    .andThen(::findCommand)
    .andThen { cmd -> checkPrivileges(loggedInUser, cmd) }
    .andThen { execute(user = loggedInUser, command = cmd, timestamp = LocalDateTime.now()) }
    .mapBoth(
        { output -> printToConsole("returned: $output") },
        { error -> printToConsole("failed to execute, reason: ${error.reason}") }
    )

Advanced Usage - Binding (Monad Comprehension)

The binding function allows multiple calls that each return a Result to be chained imperatively.

When inside a binding block, the bind() function is accessible on any Result. Each call to bind will attempt to unwrap the Result and store its value, returning early if any Result is an error.

In the example below, should functionX() return an error, then execution will skip both functionY() and functionZ(), instead storing the error from functionX in the variable named sum.

fun functionX(): Result<Int, SumError> = TODO()
fun functionY(): Result<Int, SumError> = TODO()
fun functionZ(): Result<Int, SumError> = TODO()

val sum: Result<Int, SumError> = binding {
    val x = functionX().bind()
    val y = functionY().bind()
    val z = functionZ().bind()
    x + y + z
}

println("The sum is $sum") // prints "The sum is Ok(100)"

The binding function primarily draws inspiration from Bow's binding function, however below is a list of other resources on the topic of monad comprehensions.

Coroutine Binding Support

Use of suspending functions within a coroutineBinding block requires an additional dependency:

dependencies {
    implementation("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
    implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.1.0")
}

The coroutineBinding function runs inside a coroutineScope, facilitating concurrent decomposition of work.

When any call to bind() inside the block fails, the scope fails, cancelling all other children.

The example below demonstrates a computationally expensive function that takes five milliseconds to compute being eagerly cancelled as soon as a smaller function fails in just one millisecond:

suspend fun failsIn5ms(): Result<Int, DomainErrorA> = TODO()
suspend fun failsIn1ms(): Result<Int, DomainErrorB> = TODO()

runBlocking {
    val result: Result<Int, BindingError> = coroutineBinding { // this creates a new CoroutineScope
        val x = async { failsIn5ms().bind() }
        val y = async { failsIn1ms().bind() }
        x.await() + y.await()
    }

    // result will be Err(DomainErrorB)
}

FAQs

1. What is the performance cost?

The Result type is modelled as an inline value class. This achieves zero object allocations on the happy path. A full breakdown, with example output Java code, is available in the Overhead design doc.

2. Why not use kotlin.Result from the standard library?

"kotlin.Result is half-baked"

Ilmir Usmanov, JetBrains

This library was created in Oct 2017. The JetBrains team introduced kotlin.Result to the standard library in version 1.3 of the language in Oct 2018 as an experimental feature. Initially, it could not be used as a return type as it was "intended to be used by compiler generated code only - namely coroutines".

Less than one week after stating that they "do not encourage use of kotlin.Result", the JetBrains team announced that they "will allow returning kotlin.Result from functions" in version 1.5, releasing May 2021 — three years after its introduction in 1.3. At this time, the team were deliberating on whether to guide users towards contextual receivers to replace the Result paradigm. In later years, the context receivers experiment was superseded by context parameters, which are still in an experimental state.

Michail Zarečenskij, the Lead Language Designer for Kotlin, announced at KotlinConf 2025 the development of "Rich Errors in Kotlin", providing yet another potential solution for error handling.

As of the time of writing, the KEEP for kotlin.Result states that it is "not designed to represent domain-specific error conditions". This statement should help to inform most users with their decision of adopting it as a return type for generic business logic.

"The Result class is not designed to represent domain-specific error conditions."

Kotlin Evolution and Enhancement Process #127

Reasons against kotlin.Result:

  • The functionality it provides does not match that of a first class citizen Result type found in other languages, nor the functionality offered by this library.
  • The Kotlin team admits its "half-baked" and discourages use for "domain-specific error conditions".
  • The Kotlin team do not use it, and are sending increasingly mixed messages on how users should be dealing with domain-specific errors.
  • JetBrains keep inventing their own domain-specific versions: one, two, three, four, five, six - thus proving the need for such a type but lacking commitment to a standardised solution.
  • It was initially unusable as a return type and usage was discouraged. This restriction was then lifted and users guided towards context receivers. Context receivers were abandoned in favour of the (still experimental) context parameters. Rich errors have been proposed to supersede context parameters by providing a language-level solution.
  • The runCatching implementation is incompatible with cooperatively cancelled coroutines. It catches all child types of Throwable, therefore catching a CancellationException. This is a special type of exception that "indicates normal cancellation of a coroutine". Catching and not rethrowing it will break this behaviour. This library provides runSuspendCatching to address this.
  • Error types are constrained to being subclasses of Throwable. This means you must inherit from Throwable in all of your domain-specific errors. This comes with the trappings of stacktraces being computed per-instantiation, and errors now being throwable generally across your codebase regardless of whether you intend for consumers to throw them.
  • Instantiation is verbose with factory functions being under the Result companion object: Result.success, Result.failure

Reasons for kotlin-result over kotlin.Result:

  • Consistent naming with existing Result libraries from other languages (e.g. map, mapError, mapBoth, mapEither, and, andThen, or, orElse, unwrap)
  • Feature parity with Result types from other languages including Elm, Haskell, & Rust
  • Extension functions on Iterable & List for folding, combining, partitioning
  • Monadic comprehension support via the binding and coroutineBinding functions for imperative use
  • Coroutine-aware primitives e.g. coroutineBinding and runSuspendCatching
  • Lax constraints on the error type's inheritance (does not inherit from Throwable)
  • Top-level Ok and Err functions for instantiation brevity

3. Why not call it Either?

"Either in particular, wow it is just not a beautiful thing. It does not mean OR. It's got a left and a right, it should have been called 'left right thingy'. Then you'd have a better sense of the true semantics; there are no semantics except what you superimpose on top of it."

Rich Hickey, author of Closure

Result is opinionated in name and nature with a strict definition. It models its success as the left generic parameter and failure on the right. This decision removes the need for users to choose a "biased" side which is a repeated point of contention for anyone using the more broadly named Either type. As such there is no risk of different libraries/teams/projects using different sides for bias.

Either itself is misleading and harmful. It is a naive attempt to add a true OR type to the type system. It has no pre-defined semantics, and is missing the properties of a truly mathematical OR:

  • Not Commutative: Either<String, Int> is not the same as the type Either<Int, String>. The order of the types is fixed, as the positions themselves have different conventional meanings.
  • Not Symmetric: Either<String, Int> has left and right components are not treated as equals. They are designed for different roles: String for the success value and Int for the error value. They are not interchangeable.

4. Why does runCatching catch Throwable?

For consistency with the standard libraries own runCatching.

To address the issue of breaking coroutine cancellation behaviour, we introduced the runSuspendCatching variant which explicitly rethrows any CancellationException.

Should you need to rethrow a specific type of throwable, use throwIf:

runCatching(block).throwIf { error ->
    error is IOException
}

5. I've used Result in another language, how does it translate?

Mappings are available on the wiki to assist those with experience using the Result type in other languages:

6. What other languages & libraries inspired this one?

Inspiration for this library has been drawn from other languages in which the Result monad is present, including:

Improvements on existing solutions such the stdlib include:

  • Reduced runtime overhead with zero object allocations on the happy path
  • Feature parity with Result types from other languages including Elm, Haskell, & Rust
  • Lax constraints on value/error nullability
  • Lax constraints on the error type's inheritance (does not inherit from Exception)
  • Top-level Ok and Err functions avoids qualifying usages with Result.Ok/Result.Err respectively
  • Higher-order functions marked with the inline keyword for reduced runtime overhead
  • Extension functions on Iterable & List for folding, combining, partitioning
  • Consistent naming with existing Result libraries from other languages (e.g. map, mapError, mapBoth, mapEither, and, andThen, or, orElse, unwrap)
  • Extensive test suite with almost 100 unit tests covering every library method

7. Where can I learn more?

Below is a collection of videos & articles authored on the subject of this library. Feel free to open a pull request on GitHub if you would like to include yours.

Contributing

Bug reports and pull requests are welcome on GitHub.

License

This project is available under the terms of the ISC license. See the LICENSE file for the copyright information and licensing terms.