Either<L, R>: Typed Error Handling Without Exceptions
Use Either to represent success and failure without throwing exceptions.
Either<L, R>: Typed Error Handling Without Exceptions is a free Kotlin Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Problem with Exceptions for Business Logic
Exceptions are designed for unexpected failures (null pointer, IO error). Using them to signal expected business failures (validation failure, "not found") makes control flow invisible, forces try/catch at every call site, and hides errors in function signatures.
What Is Either<L, R>?
Either is a sum type with two cases: Left(value: L) conventionally holds the error, and Right(value: R) conventionally holds the success value. A function returning Either makes the possibility of failure explicit in its signature.
Arrow's Either
Add Arrow to your project to get Either and its full ecosystem of operators:
// build.gradle.kts
implementation("io.arrow-kt:arrow-core:1.2.4")
// Usage
import arrow.core.Either
import arrow.core.left
import arrow.core.rightReturning Either from a Function
Return value.right() for success and error.left() for failure. The caller must handle both branches:
sealed class UserError { object NotFound : UserError(); data class InvalidEmail(val msg: String) : UserError() }
fun findUser(id: Long): Either<UserError, User> =
if (id <= 0) UserError.NotFound.left()
else User(id, "Alice").right()Consuming Either with fold
Use fold(ifLeft, ifRight) to handle both cases in one expression:
val result = findUser(1L)
val message = result.fold(
ifLeft = { error -> "Error: $error" },
ifRight = { user -> "Found: ${user.name}" }
)
println(message)Transforming the Right Value with map
map { } transforms the Right value without touching a Left. This makes Either a functor — you can chain transformations safely:
val nameResult: Either<UserError, String> = findUser(1L).map { it.name }Chaining with flatMap
flatMap { } chains computations that themselves return Either. If any step returns Left, the chain short-circuits and the error propagates without executing subsequent steps:
fun validateEmail(email: String): Either<UserError, String> =
if (email.contains("@")) email.right()
else UserError.InvalidEmail("bad format").left()
fun createUser(email: String): Either<UserError, User> =
validateEmail(email).flatMap { validEmail ->
User(1L, validEmail).right()
}Either.catch for Exception Wrapping
Either.catch { } runs a block and wraps any exception as a Left. Use it at the boundary between legacy code that throws and your Either-based domain:
val result: Either<Throwable, User> = Either.catch {
userRepository.findOrThrow(id)
}Recovering with getOrElse and orElse
getOrElse { default } extracts the Right value or returns a default. orElse { alternativeEither } replaces a Left with another computation:
val user: User = findUser(0L).getOrElse { User(-1L, "Guest") }
val user2: Either<UserError, User> = findUser(0L).orElse { User(99L, "Default").right() }Pattern Matching with when
Use when on a sealed Either-like class or on Either itself with is Either.Left / Either.Right to exhaustively handle cases:
when (val r = findUser(1L)) {
is Either.Left -> println("Error: ${r.value}")
is Either.Right -> println("User: ${r.value.name}")
}Either in Service Layers
Return Either from repository and service functions. Map it to HTTP responses at the Ktor route level, keeping business logic free from framework concerns:
get("/users/{id}") {
val id = call.parameters["id"]?.toLongOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest)
userService.findById(id).fold(
ifLeft = { call.respond(HttpStatusCode.NotFound) },
ifRight = { call.respond(it) }
)
}Quick Check
What does flatMap { } do on an Either.Left value?
Recap: Either<L, R>
Key takeaways:
Eithermakes the failure path explicit in the function signatureRight= success;Left= failure (by convention)map { }— transform the success value;flatMap { }— chain fallible computationsfold(ifLeft, ifRight)— consume both cases in one expressionEither.catch { }— wrap exception-throwing code at boundaries
Frequently asked questions
Is the “Either<L, R>: Typed Error Handling Without Exceptions” lesson free?
Yes — the full text of “Either<L, R>: Typed Error Handling Without Exceptions” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.
What will I learn in “Either<L, R>: Typed Error Handling Without Exceptions”?
Use Either to represent success and failure without throwing exceptions. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Kotlin Academy?
No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Either<L, R>: Typed Error Handling Without Exceptions” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Kotlin Academy lesson?
Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Either : Typed Error Handling Without Exceptions
- Arrow Raise DSL: Composing Typed Errors
- Option and Nullable: When to Use Each
- Functional Domain Modeling with Arrow's Core Types