Releases: michaelbull/kotlin-result
2.3.1
- Add fallible running accumulation functions (b319b10)
Iterable.tryRunningFoldIterable.tryRunningFoldIndexedIterable.tryRunningReduceIterable.tryRunningReduceIndexedIterable.tryScan(alias oftryRunningFold)Iterable.tryScanIndexed(alias oftryRunningFoldIndexed)
- Add destination collection variants for partition functions (4cfeec2)
Iterable.partitionToIterable.combineErrTo
- Widen
tryReducegeneric constraints generic constraints from<T, E>to<S, T : S, E>(1b84ca7)Iterable.tryReduceIterable.tryReduceIndexedFlow.tryReduce
2.3.0
- Enable Kotlin unused return value checker by @rileymichael (753785c)
- Add
@BindingDslmarker to prevent implicit outer-scopebind()(520d364) - Shadow
asynconCoroutineBindingScopeto auto-bindResult(103ac88)
Warnings for unused Result return values
The library now compiles with -Xreturn-value-checker=full, implicitly treating all public functions as @MustUseReturnValue. Consumers who enable -Xreturn-value-checker=check in their own builds will receive warnings when they silently discard a Result returned by this library.
Side-effect functions whose return value exists only for optional chaining (onOk, onErr, onEachOk, onEachErr, and their indexed/Flow variants) are marked @IgnorableReturnValue.
Preventing implicit outer-scope bind() in nested bindings
When nesting binding/coroutineBinding blocks with different error types, bind() can silently resolve to an outer scope. This compiles without warning but produces wrong control flow — the BindingException bypasses the inner scope's try/catch and short-circuits the outer scope instead:
val outer: Result<Int, String> = binding {
val inner: Result<Int, Int> = binding {
"hello".toErr().bind() // silently resolves to outer scope's bind()
}
inner.getOrElse { 0 } // never reached
}This release annotates both BindingScope and CoroutineBindingScope with a shared @DslMarker annotation, making implicit cross-scope bind() calls a compiler error. All four nesting combinations (binding/coroutineBinding in either direction) are caught. If you intentionally need to bind across scopes, explicit qualification (e.g. this@binding) is required.
Auto-binding async in coroutineBinding
Previously, the correct way to run concurrent operations in coroutineBinding required placing bind() inside the async lambda:
suspend fun provideX(): Result<Int, ExampleErr> { ... }
suspend fun provideY(): Result<Int, ExampleErr> { ... }
val result: Result<Int, ExampleErr> = coroutineBinding {
val x: Deferred<Int> = async { provideX().bind() }
val y: Deferred<Int> = async { provideY().bind() }
x.await() + y.await()
}This was non-obvious. A natural reading of the API led users to place bind() outside, chaining .await().bind() at the call site:
suspend fun provideX(): Result<Int, ExampleErr> { ... }
suspend fun provideY(): Result<Int, ExampleErr> { ... }
val result: Result<Int, ExampleErr> = coroutineBinding {
val x: Int = async { provideX() }.await().bind() // suspends here until provideX() completes
val y: Int = async { provideY() }.await().bind() // provideY() doesn't start until provideX() is done
x + y
}This compiles without warning, but chaining .await() immediately after async defeats concurrency — each call suspends until completion before the next starts.
This release adds an async member function on CoroutineBindingScope that automatically binds Result-returning lambdas. The member returns Deferred<V> instead of Deferred<Result<V, E>>, so bind() placement is handled for you and the sequential .await().bind() pattern above becomes a compiler error (there is no Result left to bind()). The correct usage is now the natural one:
suspend fun provideX(): Result<Int, ExampleErr> { ... }
suspend fun provideY(): Result<Int, ExampleErr> { ... }
val result: Result<Int, ExampleErr> = coroutineBinding {
val x: Deferred<Int> = async { provideX() }
val y: Deferred<Int> = async { provideY() }
x.await() + y.await()
}Both coroutines launch immediately and run concurrently. bind() is called inside each async coroutine, cancelling the scope on the first error without waiting for the caller to await. When the block does not return a Result, the standard CoroutineScope.async extension is used instead.
2.2.0
Fallible Operations (try*)
Fallible collection operations now use a try prefix, inspired by Gleam's naming convention and mirroring stdlib's collection API more closely. Several new operations have also been added.
- Rename
mapResult*functions totryMap*,mapAlltotryMap,fold/foldRighttotryFold/tryFoldRight(d052cf5)- The previous names are deprecated with
@Deprecated+ReplaceWithfor easy migration
- The previous names are deprecated with
- Add
tryFind,tryFindLast,tryForEach,tryForEachIndexed,tryReduce,tryReduceIndexedforIterable(d2011f0) - Add
tryFilter,tryAssociate,tryFlatMap,tryGroupBy,tryPartition+ variants forIterable(f32985e)
Flow Extensions
The kotlin-result-coroutines module now includes extensions for the kotlinx-coroutines Flow type, bringing the same Result-aware operations available on Iterable to asynchronous streams. This was born out of the discussion in #78.
- Add
Flowextensions forkotlin-coroutines(15d44a0)- Factory:
Result<Flow<V>, E>.toFlow() - Flow:
filterOk,filterErr,onEachOk,onEachErr,allOk,allErr,anyOk,anyErr,countOk,countErr,partition,combine,combineErr - Try:
tryFilter,tryFilterNot,tryMap,tryMapNotNull,tryFlatMap,tryForEach,tryReduce,tryFold,tryFind,tryFindLast,tryAssociate,tryAssociateBy,tryAssociateWith,tryGroupBy,tryPartition
- Factory:
Other
- Rename
onSuccess/onFailuretoonOk/onErr(a7c66c0)- The new indexed variants (
onEachOkIndexed,onEachErrIndexed) proved that theSuccess/Failurenaming becomes too verbose.Ok/Erris more readable.
- The new indexed variants (
- Rename
filterValues/filterErrorstofilterOk/filterErr(87f4c36)- Aligns naming with existing
allOk/allErr/anyOk/anyErr/countOk/countErrconventions
- Aligns naming with existing
- Add
onEachOk,onEachOkIndexed,onEachErr,onEachErrIndexedforIterable<Result>(a7c66c0) - Add
combineErrandcombineTo/combineErrToforIterable<Result>(1133e00, c06eeb3) - Propagate nested
coroutineBindingfailures by @dbottillo (d952b52) - Make
BindingExceptionpublic (549ed30)- Fixes
'internal' type accessed from public inline declarationwarning that will become an error in Kotlin 2.4
- Fixes
- Update Kotlin to 2.3.10 (12ceff7)
- Update Gradle to 9.4.0 (468b1a5)
2.1.0
- Annotate Result#{value,error} direct access as unsafe by @hoc081098 (db45c67)
- For full context and discussion on this topic, please read through the PR by @hoc081098 at #123, and the initial discussion presented by @kirillzh in #104.
Library consumers that directly access either Result.value or Result.error, e.g. in the situation they are extending the library to implement their own functionality, must now opt-in to unsafe access of these properties.
Failure to do so will result in a compilation error:
There are three ways to opt-in:
-
On a function-level
@OptIn(UnsafeResultValueAccess::class) fun myFunctionThatAccessesValueDirectly() { ... }
-
On a file-level:
@file:OptIn(UnsafeResultValueAccess::class) fun myFunctionThatAccessesValueDirectly() { ... } fun anotherFunctionThatAccessesValueDirectly() { ... }
-
On a project-level in
build.gradle.ktskotlin { compilerOptions { optIn.add("com.github.michaelbull.result.annotation.UnsafeResultValueAccess") optIn.add("com.github.michaelbull.result.annotation.UnsafeResultErrorAccess") } }
2.0.3
- Add
parZipby @hoc081098 (b205cf2) - Fix compiler warning for expect/actual classes by @hoangchungk53qx1 (dcb85a6)
- Add Haskell reference documentation to
combinefunctions by @hoc081098 (544cc17) - Add arrow-kt
Eitherbenchmarks by @alphaho (8262249)- Results on my machine are available in the PR comments
2.0.2
- Fix typo in README (ba7a982) by @nakamuraraika
- Update dependencies (f00e120)
- Kotlin to 2.2.0
- Kotlin Coroutines to 1.10.2
- Update Java on CI to 11 (bed111a)
- Adopt gradle-maven-publish-plugin (260d2c2)
- Publishing via OSSRH is deprecated, see #127
2.0.1
- Prevent zipOrAccumulate from executing lambdas twice by @rhirai-line (03704eb)
- Fix Arrow link in README by @hoangchungk53qx1 (aa0c0ac)
- Fix InvocationKind contract warnings by @Daiji256 (4a0c7dd)
- Fix broken link in README by @kaleidot725 (f27e020)
- Build on Zulu instead of Temurin by @eichisanden (ac929a3, 144c58e)
- Update Gradle to 8.12 (a074f26)
2.0.0
- The Result type is now an inline value class for reduced runtime overhead (981fbe2)
- Before & After comparisons outlined below
- Also see the Overhead design doc on the wiki
- Previously deprecated behaviours have been removed (eecd1b7)
Migration Guide
Ok/Err as Types
The migration to an inline value class means that using Ok/Err as types is no longer valid.
Consumers that need to introspect the type of Result should instead use Result.isOk/Result.isErr booleans. This naming scheme matches Rust's is_ok & is_err functions.
Before:
public inline fun <V, E, U> Result<V, E>.mapOrElse(default: (E) -> U, transform: (V) -> U): U {
return when (this) {
is Ok -> transform(value)
is Err -> default(error)
}
}After:
public inline fun <V, E, U> Result<V, E>.mapOrElse(default: (E) -> U, transform: (V) -> U): U {
return when {
isOk -> transform(value)
else -> default(error)
}
}Type Casting
When changing the return type to another result, e.g. the map function which goes from Result<V, E> to Result<U, E>, consumers are encouraged to use the asOk/asErr extension functions in conjunction with the isOk/isErr guard.
The example below calls asErr which unsafely casts the Result<V, E to Result<Nothing, E>, which is acceptable given the isOk check, which satisfies the Result<U, E> return type.
The asOk/asOk functions should not be used outside of a manual type guard via isOk/isErr - the cast is unsafe.
public inline infix fun <V, E, U> Result<V, E>.map(transform: (V) -> U): Result<U, E> {
return when {
isOk -> Ok(transform(value))
else -> this.asErr() // unsafely typecasts Result<V, E> to Result<Nothing, E>
}
}Removal of Deprecations
The following previously deprecated behaviours have been removed in v2.
binding&SuspendableResultBinding, usecoroutineBindinginsteadandwithout lambda argument, useandTheninsteadResultBinding, useBindingScopeinsteadgetOrwithout lambda argument, usegetOrElseinsteadgetErrorOrwithout lambda argument, usegetErrorOrElseinsteadgetAll, usefilterValuesinsteadgetAllErrors, usefilterErrorsinsteadorwithout lambda argument, useorElseinsteadResult.of, userunCatchinginsteadexpectwith non-lazy evaluation ofmessageexpectErrorwith non-lazy evaluation ofmessage
Inline Value Class - Before & After
The base Result class is now modelled as an inline value class. References to Ok<V>/Err<E> as types should be replaced with Result<V, Nothing> and Result<Nothing, E> respectively.
Calls to Ok and Err still function, but they no longer create a new instance of the Ok/Err objects - instead these are top-level functions that return a type of Result. This change achieves code that produces zero object allocations when on the "happy path", i.e. anything that returns an Ok(value). Previously, every successful operation wrapped its returned value in a new Ok(value) object.
The Err(error) function still allocates a new object each call by internally wrapping the provided error with a new instance of a Failure object. This Failure class is an internal implementation detail and not exposed to consumers. As a call to Err is usually a terminal state, occurring at the end of a chain, the allocation of a new object is unlikely to cause a lot of GC pressure unless a function that produces an Err is called in a tight loop.
Below is a comparison of the bytecode decompiled to Java produced before and after this change. The total number of possible object allocations is reduced from 4 to 1, with 0 occurring on the happy path and 1 occurring on the unhappy path.
Before: 4 object allocations, 3 on happy path & 1 on unhappy path
public final class Before {
@NotNull
public static final Before INSTANCE = new Before();
private Before() {
}
@NotNull
public final Result<Integer, ErrorOne> one() {
return (Result)(new Ok(50));
}
public final int two() {
return 100;
}
@NotNull
public final Result<Integer, ErrorThree> three(int var1) {
return (Result)(new Ok(var1 + 25));
}
public final void example() {
Result $this$map$iv = this.one(); // object allocation (1)
Result var10000;
if ($this$map$iv instanceof Ok) {
Integer var10 = INSTANCE.two();
var10000 = (Result)(new Ok(var10)); // object allocation (2)
} else {
if (!($this$map$iv instanceof Err)) {
throw new NoWhenBranchMatchedException();
}
var10000 = $this$map$iv;
}
Result $this$mapError$iv = var10000;
if ($this$mapError$iv instanceof Ok) {
var10000 = $this$mapError$iv;
} else {
if (!($this$mapError$iv instanceof Err)) {
throw new NoWhenBranchMatchedException();
}
ErrorTwo var11 = ErrorTwo.INSTANCE;
var10000 = (Result)(new Err(var11)); // object allocation (3)
}
Result $this$andThen$iv = var10000;
if ($this$andThen$iv instanceof Ok) {
int p0 = ((Number)((Ok)$this$andThen$iv).getValue()).intValue();
var10000 = this.three(p0); // object allocation (4)
} else {
if (!($this$andThen$iv instanceof Err)) {
throw new NoWhenBranchMatchedException();
}
var10000 = $this$andThen$iv;
}
String result = var10000.toString();
System.out.println(result);
}
public static abstract class Result<V, E> {
private Result() {
}
}
public static final class Ok<V> extends Result {
private final V value;
public Ok(V value) {
this.value = value;
}
public final V getValue() {
return this.value;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
Ok var10000 = (Ok)other;
return Intrinsics.areEqual(this.value, ((Ok)other).value);
} else {
return false;
}
}
public int hashCode() {
Object var10000 = this.value;
return var10000 != null ? var10000.hashCode() : 0;
}
@NotNull
public String toString() {
return "Ok(" + this.value + ')';
}
}
public static final class Err<E> extends Result {
private final E error;
public Err(E error) {
this.error = error;
}
public final E getError() {
return this.error;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
Before$Err var10000 = (Err)other;
return Intrinsics.areEqual(this.error, ((Err)other).error);
} else {
return false;
}
}
public int hashCode() {
Object var10000 = this.error;
return var10000 != null ? var10000.hashCode() : 0;
}
@NotNull
public String toString() {
return "Err(" + this.error + ')';
}
}
}After: 1 object allocation, 0 on happy path & 1 on unhappy path
public final class After {
@NotNull
public static final After INSTANCE = new After();
private After() {
}
@NotNull
public final Object one() {
return this.Ok(50);
}
public final int two() {
return 100;
}
@NotNull
public final Object three(int var1) {
return this.Ok(var1 + 25);
}
public final void example() {
Object $this$map_u2dj2AeeQ8$iv = this.one();
Object var10000;
if (Result.isOk_impl($this$map_u2dj2AeeQ8$iv)) {
var10000 = this.Ok(INSTANCE.two());
} else {
var10000 = $this$map_u2dj2AeeQ8$iv;
}
Object $this$mapError_u2dj2AeeQ8$iv = var10000;
if (Result.isErr_impl($this$mapError_u2dj2AeeQ8$iv)) {
var10000 = this.Err(ErrorTwo.INSTANCE); // object allocation (1)
} else {
var10000 = $this$mapError_u2dj2AeeQ8$iv;
}
Object $this$andThen_u2dj2AeeQ8$iv = var10000;
if (Result.isOk_impl($this$andThen_u2dj2AeeQ8$iv)) {
int p0 = ((Number) Result.getValue_impl($this$andThen_u2dj2AeeQ8$iv)).intValue();
var10000 = this.three(p0);
} else {
var10000 = $this$andThen_u2dj2AeeQ8$iv;
}
String result = Result.toString_impl(var10000);
System.out.println(result);
}
@NotNull
public final <V> Object Ok(V value) {
return Result.constructor_impl(value);
}
@NotNull
public final <E> Object Err(E error) {
...1.1.21
This release serves as a bridge towards v2 and the last major release of v1.
Old behaviours have been deprecated in a non-breaking manner to anticipate the breaking changes of v2.
Additions
- Add
flatMapEither,flatMapBoth(4e5cdee) - Add
mapCatching(15fc1ff) - Add
Iterable.allOk,Iterable.allErr,Iterable.anyOk,Iterable.anyErr,Iterable.countOk,Iterable.countErr(6e62d9f) - Add
Iterable.filterValues,Iterable.filterValuesTo,Iterable.filterErrors,Iterable.filterErrorsTo(f091f50) - Add
transpose(c46a292) - Return
Listof errors for all variants ofzipOrAccumulateby @YuitoSato (716109a)- The four-arg and five-arg variants were returning
Collectioninstead ofList.
- The four-arg and five-arg variants were returning
Deprecations
- Deprecate
getAll,getAllErrorsin favour offilterValues&filterErrors(aca9ad9) - Deprecate
ResultBindingin favour ofBindingScope(dd5c96f)- This matches the Kotlin stdlib naming convention used for sequences and coroutines.
- Deprecate suspending variant of
bindingin favour ofcoroutineBinding(b19894a)- This matches the internally-called function named
coroutineScope, and helps consumers distinguish between the blocking variant that is otherwise only differing in package name. - This should also help convey to readers that structured concurrency will occur within the block.
- This matches the internally-called function named
- Deprecate
Ok/Erras return types (7ce7c16)- This is in preparation for the v2 release where these don't exist as types.
- Deprecate
getAll/getAllErrorsin favour ofvaluesOf/errorsOf(522c821)
1.1.20
- Pin GitHub actions by commit hash (8893187)
- Add more build targets for coroutines extensions (a522fbd)
kotlinx-coroutineshas since started publishing more native build targets since we first became multiplatform. This release ensures we also build native targets for the platforms that were previously missing, namely:androidNativeArm32androidNativeArm64androidNativeX64androidNativeX86linuxArm64wasmJs
- This ensures that we are now supporting all three tiers of Kotlin/Native target support.