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.
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 for the error case:
match — handle each case explicitly:
fold — same as match but with positional arguments (error handler first, success handler second):
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:
recoverUnless lets you recover from all errors except one specific case — useful when one error type means “stop trying”:
Converting to Option
Section titled “Converting to Option”When you only care about whether an operation succeeded — not why it failed — convert to Option:
The error is discarded. Use this at boundaries where you want to fall back to Option-based logic.
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)