Either / Result
Either/Result is a sum type that models a computation which may succeed or fail. A successful outcome is represented as Ok value or Right value; a failed outcome is represented as Err error or Left error.
The key idea is that failure is not thrown outside the function as hidden control flow. It stays inside the returned value, which means the function signature can honestly describe both the happy path and the error path.
▶Architecture Diagram
🔄 ProcessDashed line animations indicate the flow direction of data or requests
Exceptions are hard to see from the call site. A function can throw from deep inside its body, but that possibility often does not appear in its signature, so callers have to remember it from documentation or from previous pain. In multi-step pipelines, exceptions also jump out of the local flow abruptly, which makes it harder to tell which stage failed and what information survived. Option is safer but often too weak: it tells you that something failed, not why. Many real application failures need both branches and the reason attached to the failing branch.
Functional languages have long preferred value-oriented error handling over exception-oriented control flow. Haskell's Either, Elm's Result, and Rust's Result made the pattern mainstream by tying it closely to the type system. Once failure becomes part of the type, the compiler and tooling can force every call site to acknowledge it. That discipline has spread into non-pure environments as well because it scales well: domain-level failures become easier to test, easier to compose, and easier to surface to users with the right message.
Either/Result works by keeping success and failure on the same track. Every call returns one value. The caller then branches on whether that value is the success variant or the error variant. Successful values can be transformed with map or chained with andThen, while failure values can be converted or logged with mapError or explicit matching. This structure makes sequencing predictable: run the next computation only if the current one is successful, otherwise propagate the existing error value unchanged. Unlike exceptions, the control flow stays local and visible in the type.
Returning a Result instead of throwing
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseAge(input: string): Result<number, string> {
const age = Number(input);
if (Number.isNaN(age)) {
return { ok: false, error: "not a number" };
}
if (age < 0) {
return { ok: false, error: "negative age is invalid" };
}
return { ok: true, value: age };
}parseAge does not throw on invalid input. It returns an explicit failure value, so the caller can see from the signature that both success and failure must be handled.
Chaining the next step only on success
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function andThen<T, E, U>(
result: Result<T, E>,
next: (value: T) => Result<U, E>
): Result<U, E> {
if (!result.ok) {
return { ok: false, error: result.error };
}
return next(result.value);
}
function toAdultLabel(age: number): Result<string, string> {
return age >= 18
? { ok: true, value: "adult" }
: { ok: false, error: "not an adult" };
}
const ok = andThen(parseAge("20"), toAdultLabel);
// { ok: true, value: "adult" }
const fail = andThen(parseAge("abc"), toAdultLabel);
// { ok: false, error: "not a number" }If the earlier step fails, the next computation is skipped and the existing error keeps flowing forward unchanged. That short-circuiting behavior is the core of Result-based pipelines.
Either/Result differs from Maybe/Option in the amount of meaning it carries. Option says only whether a value exists. Result says whether a computation succeeded and, if not, what kind of failure happened. It is also different from exceptions. Exceptions leave the normal return channel and rely on distant handlers; Result stays inside the return channel, which makes failure handling explicit and local. The tradeoff is that in languages with weak syntactic support, Result-based code can feel more verbose than exception-based code.
Either/Result is a strong fit for validation, parsing, authorization checks, data decoding, and any place where failure is expected and should be handled deliberately rather than treated as a crash. In production code it is especially useful when the next layer needs to decide how to recover or what message to show. Because the error value is preserved, logs and UI messages can be built from the same information that controlled the flow. The main practical guideline is to distinguish domain failures from catastrophic system failures: Result is excellent for the former, but wrapping every irrecoverable infrastructure fault in Result can make code noisy without improving clarity.