# kotlin-result models a success or failure as a type rather than an exception

> kotlin-result is a Kotlin Multiplatform Result monad with Ok and Err cases, combinators for mapping, chaining, binding and zipping, and separate artifacts for JVM, native, JS and WASM targets. It is ISC licensed, and the last push was on 2026-07-04.

**michaelbull/kotlin-result** — A multiplatform Result monad for modelling success or failure operations.

- Repository: https://github.com/michaelbull/kotlin-result
- Stars: 1,259 · Forks: 80
- Language: Kotlin
- License: ISC
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/michaelbull-kotlin-result

## Two cases, two railways

kotlin-result is a multiplatform Result monad for modelling success or failure operations. The README describes it in functional programming terms: the Result type is a monadic type holding a returned value or an error.

The usage is direct. To indicate that an operation succeeded you return Ok(value) with the successful value, and if it failed you return Err(error) with the error that caused the failure. The idea this encodes is what the README calls a clear happy and unhappy path of execution, commonly referred to as Railway Oriented Programming, where the happy and unhappy paths are represented as separate railways.

The example is a parser, and it is a good one because the error type is a domain type rather than a string or an exception:

```kotlin
object EmailAddressParser {

    fun parse(address: String?): Result<EmailAddress, DomainMessage> {
        return when {
            address.isNullOrBlank() -> Err(EmailRequired)
            address.length > MAX_LENGTH -> Err(EmailTooLong)
            !address.matches(PATTERN) -> Err(EmailInvalid)
            else -> Ok(EmailAddress(address))
        }
    }
}
```

Note the shape of the type. It is Result<EmailAddress, DomainMessage>, two type parameters, the success type and the error type, which is the detail that distinguishes this from the standard library's kotlin.Result, which carries a Throwable and gives you no way to say what went wrong in the type. A caller of this parser can pattern match on EmailRequired, EmailTooLong and EmailInvalid and be forced to handle them, and the compiler can tell you when you have missed one.

The examples in the README use a customer service domain, and a working application demonstrating the patterns is in the example directory, so the patterns are meant to be read as a set rather than as isolated snippets.

## Getting exceptions and nulls into the Result type

The library does not pretend that the world throws nothing. It provides the two bridges you need to get existing code into the Result world.

The first is runCatching, which captures the execution of a throwing call as a Result<T, Throwable>:

```kotlin
val result: Result<Unit, Throwable> = runCatching {
    repository.save(customer)
}
```

Note the type. The error is a Throwable here, not a domain type, which is the honest outcome: the library can capture an exception, and it cannot know what the exception means in your domain. The next step is to convert it, and the README shows mapError for that:

```kotlin
runCatching { repository.save(entity) }
    .mapError(::exceptionToDomainMessage)
```

The second bridge is toResultOr, which converts a nullable into a Result. This is the one that matters in practice, because a repository method that returns a nullable is the shape most existing code has:

```kotlin
fun findById(id: CustomerId): Result<CustomerEntity, CustomerNotFound> {
    return repository.findById(id)
        .toResultOr { CustomerNotFound }
}
```

Together these two functions mean you can adopt the library incrementally. You do not have to rewrite a repository before you can use it, you wrap its nullable return or its throwing call and then map the error into your domain type. That is a much lower barrier than the usual framing of adopting a Result type, where people imagine rewriting everything at once.

The pattern of the error type carrying domain meaning is what makes the whole thing worth the extra type parameter, and it is also what makes the error type a design decision you have to make early.

## map, mapError and mapBoth are the three transformation primitives

Three functions cover most of what you do with a Result.

map transforms the success value and leaves the error alone:

```kotlin
fun getById(id: Long): Result<CustomerDto, DomainMessage> {
    return parseCustomerId(id)
        .andThen(::findById)
        .map(::entityToDto)
}
```

mapError transforms the error into a different type and leaves the success value alone, which is the bridge from a raw exception to a domain message, as shown above.

mapBoth, also available as fold, handles both cases and produces a single value. The README shows it mapped to an HTTP response, which is the clearest illustration of why this matters:

```kotlin
val (status, body) = customerService.getById(id)
    .mapBoth(
        { customer -> HttpStatusCode.OK to customer },
        { error -> HttpStatusCode.BadRequest to error.message }
    )
```

The destructuring of the pair into status and body is what makes this readable. Without a fold, you would write an if or a when over the sealed type, and the compiler would make you handle both branches. The library gives you a name for that operation.

There is a design point in the name, fold. That is the name used in functional programming for taking a structure and reducing it to a single value by applying a function to each case, and the fact that mapBoth and fold are the same function under two names says the library is written by someone who came from that tradition. The alias is a courtesy to readers who know the term.

## andThen for linear chains, binding when the chain is not

Chaining is where the library has to make a real choice about ergonomics, and it offers two tools for two shapes of problem.

andThen chains operations where each step may fail, passing the success value from one step to the next. The example chains three steps off a parameters object:

```kotlin
val (status, body) = call.parameters
    .readId()
    .andThen(::parseCustomerId)
    .andThen(::findById)
    .map(::entityToDto)
    .mapBoth(::customerToResponse, ::messageToResponse)
```

The README says this works well for linear pipelines where each step's output feeds directly into the next. That is the accurate description and also the limitation.

When the chain is not linear, and later steps need values from earlier steps that are not the immediately preceding one, andThen forces nesting, because each step's output is the only thing in scope for the next. The README names the resulting shape the arrow anti-pattern, and shows what it looks like:

```kotlin
fun save(id: Long, dto: CustomerDto): Result<Event?, DomainMessage> {
    return parseCustomerId(id).andThen { customerId ->
        findById(customerId).andThen { existing ->
            validate(dto).andThen { validated ->
                updateEntity(customerId, existing, validated)
            }
        }
    }
}
```

The binding function is the answer. It provides an imperative-style block where each .bind() call unwraps a Result into a named variable, all intermediate values stay in scope naturally, and any failure short-circuits the entire block:

```kotlin
fun save(id: Long, dto: CustomerDto): Result<Event?, DomainMessage> = binding {
    val customerId = parseCustomerId(id).bind()
    val existing = findById(customerId).bind()
    val validated = validate(dto).bind()
    updateEntity(customerId, existing, validated)
}
```

The short-circuiting is the substantive part, not the syntax. Because bind returns from the enclosing block on failure, the later steps do not run, which is what you want, and the nesting disappears. This is the single most useful thing in the library for anyone whose domain logic has more than one independent value to carry.

## zip for the first error, zipOrAccumulate for all of them

Combining independent results has a design question attached, and the library answers it with two functions.

zip combines multiple independent results and returns early with the first error:

```kotlin
fun validate(dto: CustomerDto): Result<Customer, DomainMessage> {
    return zip(
        { PersonalNameParser.parse(dto.firstName, dto.lastName) },
        { EmailAddressParser.parse(dto.email) },
        ::Customer
    )
}
```

zipOrAccumulate does the same thing but collects all the errors instead of stopping at the first, which changes the error type of the result to a list:

```kotlin
fun validate(dto: CustomerDto): Result<Customer, List<DomainMessage>> {
    return zipOrAccumulate(
        { PersonalNameParser.parse(dto.firstName, dto.lastName) },
        { EmailAddressParser.parse(dto.email) },
        ::Customer
    )
}
```

That difference in the error type is the whole point. With zip, the caller gets one error and does not know whether the name or the email was wrong, so a form that shows a single error message has to make a second round trip. With zipOrAccumulate, the caller gets the list and can show every problem at once, and the compiler enforces that the error type has changed to List<DomainMessage>.

Both support 2 to 5 arity, so a five field validation is one call.

There is a matching set of extension functions on Iterable<Result<V, E>> for working with collections. combine turns a List<Result<V, E>> into a Result<List<V>, E>, returning early with the first error. partition splits results into a Pair of a list of values and a list of errors, so you can handle the good ones and the bad ones separately. filterOk and filterErr extract values or errors from a collection of results.

The distinction between combine and partition is worth remembering, because they solve adjacent problems. combine is the one that preserves the fail-fast semantics of the whole operation, while partition is the one that lets you keep going and deal with the failures as a batch.

## Multiplatform, separate coroutines artifact, and a maintained but slow-moving repo

The distribution story is the one piece of practical information every adopter needs, and the README gives it in Gradle coordinates:

```groovy
repositories {
    mavenCentral()
}

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

Coroutine support lives in a separate artifact, kotlin-result-coroutines, which the README points at in the Coroutines section. Splitting it out is the right call for a library, since most users do not use coroutines and should not have to depend on them.

The multiplatform story is the other differentiator. The description says the library provides all three tiers of Kotlin/Native target support, and the badge list in the README spells out what that means in practice: android, jvm, js, nodejs, linux, windows, wasm, ios, macos, tvos, watchos, js-ir, android-native and apple-silicon. The presence of the Kotlin/JS IR target and the native ones is what separates a genuinely multiplatform library from one that says multiplatform but only really supports the JVM.

The repository layout reflects that split, with a kotlin-result/ directory for the core artifact, a kotlin-result-coroutines/ directory, an example/ directory with its own build.gradle.kts, a buildSrc/ directory, a benchmarks/ directory, a kotlin-js-store/ directory, and both build.gradle.kts and settings.gradle.kts at the top level. There is also a mise.toml for tool version management and, unusually for a library, llms.txt and llms-full.txt files, which are the machine readable documentation files some tools consume.

The licence is ISC, which is short and permissive. The maintenance record is modest but real. Releases 2.2.0, 2.3.0 and 2.3.1 landed on 2026-03-11, 2026-03-12 and 2026-03-14, three releases in four days, and the last push was on 2026-07-04. The repository is not archived, so work continues, but with no releases since March, anyone pinning a version should check what has changed on master since.

## Conclusion

kotlin-result suits a Kotlin Multiplatform codebase that wants failure in the type system, with typed errors rather than a Throwable, and the same combinators compiling to JVM, native, JS and WASM. It is the wrong tool if you are on a single platform and happy with exceptions, since the library costs you an extra type parameter on every function that can fail. Before adopting it, check that the error type in your signature is one you want callers to handle exhaustively, because once the error type is in the signature, changing it is a breaking change across your API. Verify first that the artifact coordinates you need are published, since the coroutine support lives in a separate kotlin-result-coroutines artifact.

## FAQ

### What is kotlin-result?

It is a Kotlin Multiplatform Result monad for modelling success or failure operations, with a success case of Ok(value) and a failure case of Err(error), published for JVM, Kotlin/Native targets, JS and WASM. It is licensed under ISC.

### How do I add kotlin-result to a Gradle build?

Add the mavenCentral() repository and the dependency com.michael-bull.kotlin-result:kotlin-result:2.3.1. Coroutine support is a separate artifact, kotlin-result-coroutines, so the coroutine dependency is not pulled in unless you want it.

### How do I convert an exception or a nullable into a Result?

Use runCatching to capture a throwing call as a Result<T, Throwable>, and toResultOr to convert a nullable into a Result with a supplied error. You can then map the Throwable into a domain error with mapError.

### What is the difference between andThen and binding?

andThen chains steps where each output feeds the next, and it works well for linear pipelines. When later steps need earlier values, andThen forces nesting, which the README calls the arrow anti-pattern. binding gives an imperative-style block where each .bind() unwraps into a named variable, keeps intermediate values in scope and short-circuits on failure.

### What is the difference between zip and zipOrAccumulate?

zip combines independent results and returns early with the first error, while zipOrAccumulate collects all the errors instead, so the result error type becomes a list. Both support 2 to 5 arity.

## Sources

- [Issues](https://github.com/michaelbull/kotlin-result/issues)
- [License: ISC](https://github.com/michaelbull/kotlin-result/blob/master/LICENSE)
- [michaelbull/kotlin-result on GitHub](https://github.com/michaelbull/kotlin-result)
- [README](https://github.com/michaelbull/kotlin-result/blob/master/README.md)
- [Releases](https://github.com/michaelbull/kotlin-result/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/michaelbull-kotlin-result
