This chapter reframes error handling in Kotlin by moving beyond Option, which is ideal for absent-but-not-erroneous data, to sum types that preserve error causes. It contrasts product types like Pair with sum types like Either to represent “a value or an error,” adopting the convention Right for success and Left for error. Either is equipped with map, flatMap, getOrElse, and orElse to enable composition, but because its left side is unconstrained, the chapter motivates a biased, domain-appropriate alternative where failures carry meaningful diagnostics rather than being flattened away.
The Result type answers this need with Success for values and Failure for errors (typically RuntimeException), later extended with Empty to model legitimate absence distinct from failure. Composition mirrors Option/Either (map, flatMap, getOrElse, orElse), but implementations defensively catch exceptions to keep computations total and explicit about failure. Crucially, Result is treated as a computational context, not a mere container—values are combined without unwrapping, and optional cases (like a missing map entry or optional email) are modeled with Empty, while invalid input or thrown exceptions become Failure. This yields clearer program behavior and diagnostics without leaking exceptions.
Advanced patterns round out the toolkit: filter and exists for predicate-driven flow; mapFailure to enrich or transform error messages; factory helpers to build Result from nullable values or predicates; and effect-application via forEach/forEachOrElse, with defaulted handlers to separate success, failure, and emptiness at program boundaries. For composition, lifting (lift, lift2, lift3) and map2 adapt pure functions to Result, while nested flatMap “comprehensions” scale to many inputs. The overarching guidance is to compose computations with Result, capture and carry errors rather than throwing them, apply effects only at the edges, and model optional versus erroneous cases explicitly for safer, more expressive Kotlin code.
Summary
Representing the absence of data due to an error is necessary. Because the Option type doesn‘t allow this, you need the Result type. You can also use the Either type to represent data of either one type (Right) or another (Left).
Either can be mapped or flat-mapped like Option, but it can be on both sides (right or left).
Either can be biased by making one side (Left) always represent the same type (RuntimeException). This biased Either type is called Result. Success is represented by a Success subtype and failure by a Failure subtype.
One way to use the Result type is to get the wrapped value if it‘s present or to use a provided default value otherwise. The default value, if not a literal, must be lazily evaluated.
Composing Option (representing optional data) with Result (representing data or an error) is tedious. This use case is made easier by adding an Empty subtype to Result, making the Option type useless.
You can map failures if needed, such as to make error messages more explicit.
Several factory functions simplify Result creation from various situations like using nullable data, or conditional data, which is represented by data and a condition that must be fulfilled.
You can apply effects to Result through the forEach function. This function allows applying different effects to Success, Failure, and Empty.
You can lift functions from A to B (using the lift function) to operate from Result<A> to Result<B>. You can lift functions from A to (B to C) (through the lift2 function) to a function from Result<A> to (Result<B> to Result<C>).
You can use the comprehension pattern to compose any number of Result data.
FAQ
Why isn’t Option ideal for handling errors?Option cleanly represents “value or no value,” but it discards the reason why data is missing. When absence is due to an error (bad input, I/O failure, etc.), Option forces callers to guess the cause. You need a type that can carry either the value or the error information, so you can react appropriately.What’s the difference between a product type (Pair) and a sum type (Either) for error handling?A product type like Pair<T, U> represents “T and U,” which doesn’t match the idea of “value or error.” A sum type like Either<E, A> represents “E or A,” which is exactly what you want: either an error E or a success value A, but never both at once.What is Either and how are Left and Right used?Either<E, A> models a value that can be one of two types. By convention, Right holds the successful value (A), and Left holds the error (E). You choose the error type (for example, String or Exception) and propagate it through your computations.How do I compose functions returning Either?Use map to transform the Right value without changing the Left, and flatMap to chain computations that themselves return Either. Provide safe fallbacks with getOrElse (to recover a raw value) or orElse (to recover with another Either). A “biased” Either means these operations target the Right side by default.What is Result and why prefer it for error handling?Result<A> specializes the idea of “success or error” with subclasses Success(value) and Failure(exception). It standardizes on RuntimeException (or a wrapped Exception) for failures, making composition, mapping, and error propagation consistent. It’s a focused alternative to Either when one branch is always “an error.”How do I access data inside a Result safely?Compose with map and flatMap to stay within Result. When you must escape, use getOrElse(default) for an eager fallback value or orElse { ... } when computing a fallback may fail and should return a Result. Prefer composition over extraction to keep error handling intact.What is Result.Empty and when should I use it?Result.Empty models “optional absence” distinct from Failure. Use Empty when missing data is a normal case (for example, a map key not present or an entity without an optional field). Reserve Failure for genuine errors (invalid input, exceptions, invariants broken), so callers can tell optional absence from failures.How do I validate Result values with predicates?Use filter to keep a Success only if a predicate holds; otherwise produce Empty or a Failure with a message. Use exists to query whether the Success value satisfies a predicate (returns true/false), without extracting it. These helpers prevent scattering conditional logic throughout your code.How can I transform or annotate failures?Use mapFailure(message) to replace or enrich the error message while preserving the original exception as a cause. You can also map Empty to Failure when a missing value is unacceptable in a given context. This lets you add domain-specific context (paths, ids, inputs) to error reports.How do I combine multiple Results and lift normal functions?Lift turns a pure function f: (A) -> B into (Result<A>) -> Result<B>. Use lift2 and lift3 (and the same pattern for more args) to apply multi-argument functions to multiple Results, or map2(a, b, f) as a convenience. For many inputs, use a “comprehension” pattern: nest flatMap calls and end with map to construct the final value.
pro $24.99 per month
access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!