kotlin.Result for Kotlin Multiplatform, usable from Swift — for KMP libraries
whose APIs return results to Swift callers.
kotlin.Result is a value class, and Kotlin/Native's Objective-C export erases
it to an untyped Any?, so a KMP library can't return one to Swift.
KotlinResult's Result<T> is a thin reference class that wraps kotlin.Result
and forwards to it:
- Kotlin gets the stdlib
ResultAPI and behaviour —isSuccess,getOrThrow,fold,map,recover,onFailure, … with the stdlib's contracts — as members, so there's nothing to import beyondResult. - Swift sees
KotlinResult<T>with helpers:try r.get()returns the value or throws the Kotlin exception itself, andr.result(as:)gives aSwift.Result. - Structured concurrency is left alone: nothing is classified "fatal", and cancellation is never captured into a result.
- No dependencies beyond the Kotlin standard library.
// gradle/libs.versions.toml
[libraries]
kotlinresult = { module = "com.happycodelucky.kotlinresult:kotlinresult", version = "1.1.0" }
kotlinresult-testing = { module = "com.happycodelucky.kotlinresult:kotlinresult-testing", version = "1.1.0" }
// build.gradle.kts
commonMain.dependencies { api(libs.kotlinresult) } // it's in your public API
commonTest.dependencies { implementation(libs.kotlinresult.testing) } // assertSuccess / assertFailureSwift reaches KotlinResult through your library's framework. Build that
framework with SKIE, and export KotlinResult into
it:
kotlin {
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.Framework>().configureEach {
export(libs.kotlinresult)
}
}
}The Swift helpers ship inside the library, and SKIE compiles them into your
framework — but only where KotlinResult keeps its plain Swift name, which the
export guarantees. Without it, the link fails with
cannot find type 'KotlinResult' in scope. Don't declare your own
extension KotlinThrowable: Error; KotlinResult provides it.
Return a Result<T>, failing with the exceptions you handle — ideally a sealed
hierarchy, so callers can match every case:
import com.happycodelucky.kotlinresult.Result
sealed class PortException(message: String) : Exception(message) {
class NotANumber(val text: String) : PortException("not a number: $text")
class OutOfRange(val port: Int) : PortException("out of range: $port")
}
fun parsePort(text: String): Result<Int> {
val port = text.toIntOrNull() ?: return Result.failure(PortException.NotANumber(text))
return if (port in 0..65535) Result.success(port) else Result.failure(PortException.OutOfRange(port))
}In files that import it, Result means KotlinResult's; write the stdlib type as
kotlin.Result. Convert between them with toStdlibResult() and toResult().
The kotlin.Result API, as members:
parsePort("8080")
.onSuccess { port -> println("listening on $port") }
.onFailure { e ->
when (e as PortException) {
is PortException.NotANumber -> println("not a number: ${e.text}")
is PortException.OutOfRange -> println("out of range: ${e.port}")
}
}
val port = parsePort(input).getOrDefault(8080)One difference from the stdlib: getOrElse, getOrDefault, recover and
recoverCatching return T, not a supertype of it. To widen, use fold or
getOrNull() ?: default.
get() returns the value — name its type — or throws the Kotlin exception, which
you catch by class and switch over exhaustively with SKIE's onEnum(of:):
do {
let port: Int = try parsePort(text: "8080").get()
print("listening on \(port)")
} catch let e as PortException {
switch onEnum(of: e) {
case .notANumber(let x): print("not a number: \(x.text)")
case .outOfRange(let x): print("out of range: \(x.port)")
}
}try result.get()on aKotlinResult<KotlinUnit>just returns or throws.result.get(as: Int.self)names the type inline, andresult.result(as: Int.self)gives aSwift.Result<Int, any Error>.- A value of the wrong type throws
KotlinResultTypeMismatchError. - Swift task cancellation reaches Kotlin as coroutine cancellation and comes
back as
CancellationError, never as a failed result.
kotlinresult-testing adds assertions that work with any test framework
(kotlin.test, JUnit, Kotest):
val port = parsePort("8080").assertSuccess() // the value, or fails with the stack trace
val error = parsePort("nope").assertFailure<PortException.NotANumber>() // the exception, typedIn Swift, build a result to return from a fake:
let ok = KotlinResult<NSString>(value: "hi")
let failed = KotlinResult<KotlinUnit>(failure: KotlinThrowable(message: "boom"))| Kotlin | 2.4 |
| JVM | 21+. Code that calls Result's inline members (fold, map, getOrElse, …) must compile with jvmTarget 21 or higher. |
| Android | minSdk 30; compile against SDK 34+. |
| iOS / macOS | iosArm64, iosSimulatorArm64, macosArm64, through your framework (built with SKIE, exporting KotlinResult). Your framework sets the minimum OS. |
The documentation site has guides, per-platform notes, the changelog, and the API reference.
See CONTRIBUTING.md.