Result — handling failures
Every function that can fail has two possible outcomes, but only one is in the type signature. The
error leaks out as an exception — invisible to callers, untraceable through the type system.
Result<E, A> puts both outcomes in the type. Callers know an operation can fail, the compiler
tracks what’s handled, and errors compose the same way values do.
flowchart TB
A([operation]) --> B{succeeded?}
B -->|"yes — Ok"| C[run next steps]
B -->|"no — Err"| D[nothing runs]
C --> E{still Ok?}
E -->|yes| F[your result]
E -->|no| G[use fallback]
D --> G
The problem with exceptions
Section titled “The problem with exceptions”try/catch has a fundamental issue: nothing in the type of a function tells you it can throw. A
function typed as (s: string) => number might throw at runtime, but the type gives no hint. Every
caller has to either know this and wrap in try/catch, or learn the hard way.
This leads to one of two outcomes — either error handling is scattered across every call site:
Or it’s skipped entirely and exceptions bubble up to a top-level handler that can’t do anything meaningful with them.
The Result approach
Section titled “The Result approach”With Result, the possibility of failure is part of the type. A function that might fail returns
Result<E, A> instead of A. The error type E is explicit — callers know exactly what can go
wrong:
The map step only runs if parseInput returned Ok. If it returned Err, the error flows
through unchanged to getOrElse, which provides the fallback. No try/catch. No conditional checks.
Creating Results
Section titled “Creating Results”The error type in Result<E, A> can be anything — a string, a discriminated union, an Error object.
You choose what fits your domain.
Wrapping throwing code with tryCatch
Section titled “Wrapping throwing code with tryCatch”When working with APIs that throw (like JSON.parse), tryCatch converts a throwing function into
a Result:
The second argument maps the caught exception to your error type, so the result is typed correctly.
Transforming values with map
Section titled “Transforming values with map”map transforms the success value, leaving Err untouched:
Chaining multiple map calls only continues while the result remains Ok. The first Err
short-circuits the rest:
Transforming errors with mapError
Section titled “Transforming errors with mapError”mapError is the counterpart to map — it transforms the error value, leaving Ok untouched:
This is useful for normalizing errors from different sources into a single error type before they reach the boundary of your system.
Chaining with chain
Section titled “Chaining with chain”When a transformation might itself fail, use chain instead of map. It prevents nested
Result<E, Result<E, A>>:
A typical pipeline chains multiple steps that can each fail independently:
If any step returns Err, subsequent steps are skipped and the error propagates to the end.
Extracting the value
Section titled “Extracting the value”getOrElse — provide a fallback as a thunk () => B. The thunk is only called when the
Result is Err, so expensive or side-effectful defaults are never computed unnecessarily. The
fallback can be a different type, widening the result to the union of both:
match — handle each case explicitly. fold is the positional form of the same thing — error
handler first, success handler second — useful when you’d rather not name the cases:
Recovering from errors
Section titled “Recovering from errors”recover provides a fallback Result when the current one is Err. Unlike getOrElse, the
fallback is itself a Result — useful when the recovery operation might also fail. The fallback
can produce a different success type, widening the result to Result<E, A | B>:
recoverUnless lets you recover from all errors except one specific case — useful when one error
type means “stop trying”:
Converting to Maybe
Section titled “Converting to Maybe”When you only care about whether an operation succeeded — not why it failed — convert to Maybe:
The error is discarded. Use this at boundaries where you want to fall back to Maybe-based logic.
One thing to watch out for: errors don’t accumulate in a Result chain — if validateName and
validateEmail both fail, you’ll only see the first. For collecting all failures at once, use
Validation instead.
sequenceDiagram
participant P as your code
participant V1 as check range
participant V2 as look up record
participant G as fallback
P->>V1: Ok("42")
V1->>V2: Ok(42)
V2->>G: Ok(record)
G-->>P: record.name
P->>V1: Ok("-1")
V1-->>V2: Err("Must be positive")
Note over V2: skipped
V2-->>G: Err
Note over G: skipped
G-->>P: "Unknown"
When to use Result vs try/catch
Section titled “When to use Result vs try/catch”Use Result when:
- The function is part of a pipeline and callers need to compose over success or failure
- Multiple operations can fail and you want a single linear flow rather than nested try/catch
- The error type matters — you want callers to know what can go wrong from the type signature alone
Keep using try/catch when:
- You’re handling truly unexpected runtime errors (out of memory, unrecoverable state)
- You’re at the very top of the call stack and just need to log and exit
- You’re interfacing with code that expects exceptions (use
tryCatchat the boundary to convert back toResult)