
Zero-overhead Result monad for modelling success or failure in operations, offering features like chaining, transformation, and binding support, inspired by Elm, Haskell, and Rust.
A multiplatform Result monad for modelling success or failure operations, providing all three tiers of Kotlin/Native target support.
repositories {
mavenCentral()
}
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:2.3.1")
}A separate kotlin-result-coroutines artifact is available for coroutine support, shown in the
Coroutines section.
In functional programming, the 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 defines a clear happy/unhappy path of execution commonly referred to as Railway Oriented Programming, whereby the happy and unhappy paths are represented as separate railways.
The examples below use a customer service domain. A working application demonstrating these
patterns is available in the example directory.
Return Ok or Err to indicate success or failure. A function that validates and parses an email
address might look like:
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))
}
}
}When interacting with code that may throw exceptions, wrap the call with
runCatching to capture its execution as a Result<T, Throwable>:
val result: Result<Unit, Throwable> = runCatching {
repository.save(customer)
}Nullable types can be converted to a Result with toResultOr:
fun findById(id: CustomerId): Result<CustomerEntity, CustomerNotFound> {
return repository.findById(id)
.toResultOr { CustomerNotFound }
}Use map to transform a success value:
fun getById(id: Long): Result<CustomerDto, DomainMessage> {
return parseCustomerId(id)
.andThen(::findById)
.map(::entityToDto)
}Use mapError to transform an error into a different type:
runCatching { repository.save(entity) }
.mapError(::exceptionToDomainMessage)Use mapBoth (also available as fold) to handle both cases and
produce a single value. This is useful for mapping a Result to an HTTP response:
val (status, body) = customerService.getById(id)
.mapBoth(
{ customer -> HttpStatusCode.OK to customer },
{ error -> HttpStatusCode.BadRequest to error.message }
)Use andThen to chain operations where each step may fail, passing the success
value from one step to the next:
val (status, body) = call.parameters
.readId()
.andThen(::parseCustomerId)
.andThen(::findById)
.map(::entityToDto)
.mapBoth(::customerToResponse, ::messageToResponse)This works well for linear pipelines where each step's output feeds directly into the next.
When a chain is not linear, later steps may need values from earlier steps that aren't the
immediately preceding one. With andThen, this forces nesting to keep intermediate values in scope,
producing the arrow anti-pattern:
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 solves this by providing 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:
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)
}Use zip to combine multiple independent results, returning early with the first
error:
fun validate(dto: CustomerDto): Result<Customer, DomainMessage> {
return zip(
{ PersonalNameParser.parse(dto.firstName, dto.lastName) },
{ EmailAddressParser.parse(dto.email) },
::Customer
)
}Use zipOrAccumulate to combine results while collecting all errors
instead of stopping at the first:
fun validate(dto: CustomerDto): Result<Customer, List<DomainMessage>> {
return zipOrAccumulate(
{ PersonalNameParser.parse(dto.firstName, dto.lastName) },
{ EmailAddressParser.parse(dto.email) },
::Customer
)
}Both zip and zipOrAccumulate support 2-5 arity.
Extension functions on Iterable<Result<V, E>> make it straightforward to work with collections
of results.
Use combine to turn a List<Result<V, E>> into a Result<List<V>, E>,
returning early with the first error:
val results: List<Result<EmailAddress, DomainMessage>> =
addresses.map(EmailAddressParser::parse)
val combined: Result<List<EmailAddress>, DomainMessage> = results.combine()Use partition to split results into a Pair<List<V>, List<E>>:
val (validAddresses, errors) = addresses
.map(EmailAddressParser::parse)
.partition()Use filterOk and filterErr to extract values or errors:
val validAddresses: List<EmailAddress> = results.filterOk()
val errors: List<DomainMessage> = results.filterErr()Additional collection functions include allOk, anyOk, countOk, countErr, onEachOk, and
onEachErr. See the full list in Iterable.kt.
The kotlin-result-coroutines module provides coroutine-aware extensions:
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:2.3.1")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1")
}coroutineBinding is the concurrent equivalent of binding. It runs
inside a coroutineScope, enabling concurrent decomposition of work. When
any call to bind() fails, the scope is cancelled, cancelling all other children:
suspend fun fetchCustomerProfile(id: CustomerId): Result<CustomerProfile, DomainMessage> {
return coroutineBinding {
val customer = async { findById(id) }
val orders = async { findOrderHistory(id) }
CustomerProfile(customer.await(), orders.await())
}
}runSuspendCatching is a coroutine-safe variant of runCatching.
The standard library's runCatching catches CancellationException, which breaks cooperative
coroutine cancellation. runSuspendCatching rethrows it:
suspend fun findCustomer(id: CustomerId): Result<CustomerEntity, Throwable> {
return runSuspendCatching {
repository.findById(id)
}
}Extension functions on Flow<Result<V, E>> mirror the collection extensions: filterOk,
filterErr, onEachOk, onEachErr, combine, and partition. See the full list in
Flow.kt.
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.
"
kotlin.Resultis half-baked"
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."
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.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.Result companion object: Result.success,
Result.failure
map, mapError, mapBoth, mapEither,
and, andThen, or, orElse, unwrap)Iterable & List for folding, combining, partitioningbinding and coroutineBinding functions for imperative usecoroutineBinding and runSuspendCatching
error type's inheritance (does not inherit from Throwable)Ok and Err functions for instantiation brevity"
Eitherin 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."
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:
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.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.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
}Mappings are available on the wiki to assist those with experience using the Result type in other languages:
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:
value/error nullabilityerror type's inheritance (does not inherit from Exception)Ok and Err functions avoids qualifying usages with Result.Ok/Result.Err respectivelyinline keyword for reduced runtime overheadIterable & List for folding, combining, partitioningmap, mapError, mapBoth, mapEither,
and, andThen, or, orElse, unwrap)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.
Bug reports and pull requests are welcome on GitHub.
This project is available under the terms of the ISC license. See the LICENSE file for the copyright
information and licensing terms.
A multiplatform Result monad for modelling success or failure operations, providing all three tiers of Kotlin/Native target support.
repositories {
mavenCentral()
}
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:2.3.1")
}A separate kotlin-result-coroutines artifact is available for coroutine support, shown in the
Coroutines section.
In functional programming, the 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 defines a clear happy/unhappy path of execution commonly referred to as Railway Oriented Programming, whereby the happy and unhappy paths are represented as separate railways.
The examples below use a customer service domain. A working application demonstrating these
patterns is available in the example directory.
Return Ok or Err to indicate success or failure. A function that validates and parses an email
address might look like:
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))
}
}
}When interacting with code that may throw exceptions, wrap the call with
runCatching to capture its execution as a Result<T, Throwable>:
val result: Result<Unit, Throwable> = runCatching {
repository.save(customer)
}Nullable types can be converted to a Result with toResultOr:
fun findById(id: CustomerId): Result<CustomerEntity, CustomerNotFound> {
return repository.findById(id)
.toResultOr { CustomerNotFound }
}Use map to transform a success value:
fun getById(id: Long): Result<CustomerDto, DomainMessage> {
return parseCustomerId(id)
.andThen(::findById)
.map(::entityToDto)
}Use mapError to transform an error into a different type:
runCatching { repository.save(entity) }
.mapError(::exceptionToDomainMessage)Use mapBoth (also available as fold) to handle both cases and
produce a single value. This is useful for mapping a Result to an HTTP response:
val (status, body) = customerService.getById(id)
.mapBoth(
{ customer -> HttpStatusCode.OK to customer },
{ error -> HttpStatusCode.BadRequest to error.message }
)Use andThen to chain operations where each step may fail, passing the success
value from one step to the next:
val (status, body) = call.parameters
.readId()
.andThen(::parseCustomerId)
.andThen(::findById)
.map(::entityToDto)
.mapBoth(::customerToResponse, ::messageToResponse)This works well for linear pipelines where each step's output feeds directly into the next.
When a chain is not linear, later steps may need values from earlier steps that aren't the
immediately preceding one. With andThen, this forces nesting to keep intermediate values in scope,
producing the arrow anti-pattern:
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 solves this by providing 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:
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)
}Use zip to combine multiple independent results, returning early with the first
error:
fun validate(dto: CustomerDto): Result<Customer, DomainMessage> {
return zip(
{ PersonalNameParser.parse(dto.firstName, dto.lastName) },
{ EmailAddressParser.parse(dto.email) },
::Customer
)
}Use zipOrAccumulate to combine results while collecting all errors
instead of stopping at the first:
fun validate(dto: CustomerDto): Result<Customer, List<DomainMessage>> {
return zipOrAccumulate(
{ PersonalNameParser.parse(dto.firstName, dto.lastName) },
{ EmailAddressParser.parse(dto.email) },
::Customer
)
}Both zip and zipOrAccumulate support 2-5 arity.
Extension functions on Iterable<Result<V, E>> make it straightforward to work with collections
of results.
Use combine to turn a List<Result<V, E>> into a Result<List<V>, E>,
returning early with the first error:
val results: List<Result<EmailAddress, DomainMessage>> =
addresses.map(EmailAddressParser::parse)
val combined: Result<List<EmailAddress>, DomainMessage> = results.combine()Use partition to split results into a Pair<List<V>, List<E>>:
val (validAddresses, errors) = addresses
.map(EmailAddressParser::parse)
.partition()Use filterOk and filterErr to extract values or errors:
val validAddresses: List<EmailAddress> = results.filterOk()
val errors: List<DomainMessage> = results.filterErr()Additional collection functions include allOk, anyOk, countOk, countErr, onEachOk, and
onEachErr. See the full list in Iterable.kt.
The kotlin-result-coroutines module provides coroutine-aware extensions:
dependencies {
implementation("com.michael-bull.kotlin-result:kotlin-result:2.3.1")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1")
}coroutineBinding is the concurrent equivalent of binding. It runs
inside a coroutineScope, enabling concurrent decomposition of work. When
any call to bind() fails, the scope is cancelled, cancelling all other children:
suspend fun fetchCustomerProfile(id: CustomerId): Result<CustomerProfile, DomainMessage> {
return coroutineBinding {
val customer = async { findById(id) }
val orders = async { findOrderHistory(id) }
CustomerProfile(customer.await(), orders.await())
}
}runSuspendCatching is a coroutine-safe variant of runCatching.
The standard library's runCatching catches CancellationException, which breaks cooperative
coroutine cancellation. runSuspendCatching rethrows it:
suspend fun findCustomer(id: CustomerId): Result<CustomerEntity, Throwable> {
return runSuspendCatching {
repository.findById(id)
}
}Extension functions on Flow<Result<V, E>> mirror the collection extensions: filterOk,
filterErr, onEachOk, onEachErr, combine, and partition. See the full list in
Flow.kt.
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.
"
kotlin.Resultis half-baked"
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."
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.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.Result companion object: Result.success,
Result.failure
map, mapError, mapBoth, mapEither,
and, andThen, or, orElse, unwrap)Iterable & List for folding, combining, partitioningbinding and coroutineBinding functions for imperative usecoroutineBinding and runSuspendCatching
error type's inheritance (does not inherit from Throwable)Ok and Err functions for instantiation brevity"
Eitherin 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."
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:
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.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.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
}Mappings are available on the wiki to assist those with experience using the Result type in other languages:
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:
value/error nullabilityerror type's inheritance (does not inherit from Exception)Ok and Err functions avoids qualifying usages with Result.Ok/Result.Err respectivelyinline keyword for reduced runtime overheadIterable & List for folding, combining, partitioningmap, mapError, mapBoth, mapEither,
and, andThen, or, orElse, unwrap)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.
Bug reports and pull requests are welcome on GitHub.
This project is available under the terms of the ISC license. See the LICENSE file for the copyright
information and licensing terms.