Functional Error Handling with Either
Use Either and Option for type-safe error handling: Railway-oriented programming without exceptions.
// Minimal Either implementation (or use Arrow library)
sealed class Either<out L, out R> {
data class Left<out L>(val value: L) : Either<L, Nothing>()
data class Right<out R>(val value: R) : Either<Nothing, R>()
fun <T> fold(onLeft: (L) -> T, onRight: (R) -> T): T = when (this) {
is Left -> onLeft(value)
is Right -> onRight(value)
}
fun <T> map(f: (R) -> T): Either<L, T> = when (this) {
is Left -> this
is Right -> Right(f(value))
}
fun <T> flatMap(f: (R) -> Either<L, T>): Either<L, T> = when (this) {
is Left -> this
is Right -> f(value)
}
fun getOrElse(default: () -> @UnsafeVariance R): R = when (this) {
is Left -> default()
is Right -> value
}
val isRight get() = this is Right
val isLeft get() = this is Left
}
fun <L, R> Either<L, R>.orElse(alt: () -> Either<L, R>): Either<L, R> = when (this) {
is Either.Left -> alt()
is Either.Right -> this
}
// Domain errors
sealed class AppError(val message: String) {
class NotFound(entity: String, id: String) : AppError("$entity '$id' not found")
class Validation(msg: String) : AppError(msg)
class Unauthorized(msg: String = "Not authorized") : AppError(msg)
class Database(msg: String) : AppError("DB error: $msg")
}
// Domain
data class Email private constructor(val value: String) {
companion object {
fun create(raw: String): Either<AppError, Email> {
if (raw.isBlank()) return Either.Left(AppError.Validation("Email required"))
if (!raw.contains("@")) return Either.Left(AppError.Validation("Invalid email"))
if (raw.length > 255) return Either.Left(AppError.Validation("Email too long"))
return Either.Right(Email(raw.lowercase().trim()))
}
}
}
data class User(val id: String, val name: String, val email: Email)
// Repository
class UserRepo {
private val users = mutableMapOf<String, User>()
fun save(user: User): Either<AppError, User> = try {
users[user.id] = user
Either.Right(user)
} catch (e: Exception) {
Either.Left(AppError.Database(e.message ?: "Unknown"))
}
fun findById(id: String): Either<AppError, User> =
users[id]?.let { Either.Right(it) }
?: Either.Left(AppError.NotFound("User", id))
}
// Service — railway-oriented
class UserService(private val repo: UserRepo) {
fun createUser(id: String, name: String, rawEmail: String): Either<AppError, User> {
if (name.isBlank()) return Either.Left(AppError.Validation("Name required"))
if (name.length > 50) return Either.Left(AppError.Validation("Name too long"))
return Email.create(rawEmail)
.map { email -> User(id, name.trim(), email) }
.flatMap { user -> repo.save(user) }
}
fun getUser(id: String): Either<AppError, User> = repo.findById(id)
fun getUserEmail(id: String): Either<AppError, String> =
repo.findById(id).map { it.email.value }
}
// Combine multiple Either values
fun <L, A, B, C> zip(
a: Either<L, A>,
b: Either<L, B>,
f: (A, B) -> C
): Either<L, C> = a.flatMap { av -> b.map { bv -> f(av, bv) } }
fun main() {
val repo = UserRepo()
val service = UserService(repo)
// Success path
val result1 = service.createUser("1", "Alice", "alice@test.com")
result1.fold(
onLeft = { println("Error: ${it.message}") },
onRight = { println("Created: $it") }
)
// Validation error
val result2 = service.createUser("2", "", "bob@test.com")
result2.fold(
onLeft = { println("Error: ${it.message}") },
onRight = { println("Created: $it") }
)
// Invalid email
val result3 = service.createUser("3", "Charlie", "not-email")
println("Result3: ${result3.fold({ it.message }, { it.name })}")
// Chain operations
val email = service.getUserEmail("1")
println("Email: ${email.fold({ it.message }, { it })}")
// Not found
val missing = service.getUser("999")
println("Missing: ${missing.fold({ it.message }, { it })}")
// getOrElse
val userName = service.getUser("999")
.map { it.name }
.getOrElse { "Unknown" }
println("Name: $userName")
// Pattern matching with when
when (val r = service.getUser("1")) {
is Either.Left -> when (r.value) {
is AppError.NotFound -> println("Not found")
is AppError.Validation -> println("Invalid")
is AppError.Unauthorized -> println("Unauthorized")
is AppError.Database -> println("DB Error")
}
is Either.Right -> println("Found: ${r.value.name}")
}
}Use Cases
- Type-safe error handling without exceptions
- Railway-oriented business logic
- Composable validation pipelines
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
Functional Error Handling with Either
Implement Either monad for type-safe error handling with map, flatMap, and fold operations.
Best for: Type-safe error handling without exceptions
Result Monad — Functional Error Handling
Handle errors functionally with Kotlin Result: map, recover, fold, and chaining fallible operations.
Best for: Type-safe error handling without exceptions
Kotlin Result API Functional Error Handling
Use Kotlin's built-in Result type for functional error handling: runCatching, map, recover, and fold.
Best for: Functional error handling without try-catch
Collections — map, filter, groupBy, and More
Master Kotlin collections: functional transformations, aggregations, grouping, and partition operations.
Best for: Data processing and transformation pipelines