kotlinintermediate

Result Monad — Functional Error Handling

Handle errors functionally with Kotlin Result: map, recover, fold, and chaining fallible operations.

kotlin
// Using Kotlin's built-in Result type
fun parseInt(s: String): Result<Int> = runCatching { s.toInt() }

fun divide(a: Int, b: Int): Result<Double> = runCatching {
    require(b != 0) { "Division by zero" }
    a.toDouble() / b
}

// Custom Result with sealed class for more control
sealed class Outcome<out T> {
    data class Ok<T>(val value: T) : Outcome<T>()
    data class Err(val error: AppError) : Outcome<Nothing>()

    fun <R> map(transform: (T) -> R): Outcome<R> = when (this) {
        is Ok -> Ok(transform(value))
        is Err -> this
    }

    fun <R> flatMap(transform: (T) -> Outcome<R>): Outcome<R> = when (this) {
        is Ok -> transform(value)
        is Err -> this
    }

    fun recover(handler: (AppError) -> T): T = when (this) {
        is Ok -> value
        is Err -> handler(error)
    }

    fun <R> fold(onOk: (T) -> R, onErr: (AppError) -> R): R = when (this) {
        is Ok -> onOk(value)
        is Err -> onErr(error)
    }

    fun getOrNull(): T? = (this as? Ok)?.value
    fun getOrDefault(default: @UnsafeVariance T): T = (this as? Ok)?.value ?: default
    fun getOrThrow(): T = when (this) {
        is Ok -> value
        is Err -> throw error.toException()
    }
}

sealed class AppError(val message: String) {
    class NotFound(val id: String) : AppError("Not found: $id")
    class Validation(val field: String, val reason: String) : AppError("$field: $reason")
    class Network(val url: String, cause: String) : AppError("Network error at $url: $cause")
    class Unauthorized : AppError("Authentication required")

    fun toException() = RuntimeException(message)
}

// Service layer using Outcome
data class User(val id: String, val name: String, val email: String)

object UserRepository {
    private val users = mapOf(
        "1" to User("1", "Alice", "alice@test.com"),
        "2" to User("2", "Bob", "bob@test.com")
    )

    fun findById(id: String): Outcome<User> =
        users[id]?.let { Outcome.Ok(it) } ?: Outcome.Err(AppError.NotFound(id))

    fun validate(user: User): Outcome<User> = when {
        user.name.isBlank() -> Outcome.Err(AppError.Validation("name", "cannot be blank"))
        !user.email.contains("@") -> Outcome.Err(AppError.Validation("email", "invalid format"))
        else -> Outcome.Ok(user)
    }
}

fun main() {
    // Built-in Result
    val r1 = parseInt("42")
    val r2 = parseInt("abc")
    println("Parse 42: ${r1.getOrNull()}")    // 42
    println("Parse abc: ${r2.getOrNull()}")    // null

    // Result chaining
    val computed = parseInt("10")
        .mapCatching { it * 2 }
        .mapCatching { it / 0 } // ArithmeticException
        .recover { -1 }
    println("Computed: $computed") // -1

    // fold
    val message = parseInt("abc").fold(
        onSuccess = { "Got: $it" },
        onFailure = { "Error: ${it.message}" }
    )
    println(message)

    // Custom Outcome
    println("\n--- Custom Outcome ---")
    val user = UserRepository.findById("1")
        .flatMap { UserRepository.validate(it) }
        .map { it.copy(name = it.name.uppercase()) }

    user.fold(
        onOk = { println("User: $it") },
        onErr = { println("Error: ${it.message}") }
    )

    // Not found
    UserRepository.findById("99").fold(
        onOk = { println("Found: $it") },
        onErr = { println("Error: ${it.message}") } // Not found: 99
    )

    // recover with default
    val fallback = UserRepository.findById("99")
        .recover { User("0", "Guest", "guest@test.com") }
    println("Recovered: $fallback")
}

Use Cases

  • Type-safe error handling without exceptions
  • Service layer error propagation
  • Chaining fallible operations

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.