Result — Modeling Failures
Every function that can fail has two possible outcomes: it either succeeds and returns a value, or it fails and returns an error. Yet, in traditional JavaScript, only the success outcome is visible in the function’s type signature. The failure is treated as an exceptional event, thrown out of the call stack as a runtime exception.
This model has a fundamental structural flaw: exceptions are invisible to the type system. A
function typed as (id: string) => User might throw at runtime if the database is down or the id is
invalid, but nothing in its type forces the caller to handle that failure. Callers must either read
the source code, read outdated documentation, or learn the hard way when the application crashes in
production.
Result<E, A> solves this by making the possibility of failure an explicit, first-class value. It
puts both outcomes inside the type system: Ok<A> representing a successful value, and Err<E>
representing a typed error.
flowchart TD
Raw[Raw Operation] --> Choice{Did it succeed?}
Choice -->|Yes| Ok[Ok A]
Choice -->|No| Err[Err E]
Ok --> Map[Apply Transformations]
Err --> Skip[Skip / Propagate Error]
Map --> Edge[Extract / Handle both cases]
Skip --> Edge
By turning exceptions into a data structure, we can chain operations safely. If any step fails, the error flows through to the end of the pipeline automatically, while the happy path is skipped. We decouple the logic of what we want to do from the logic of handling failures.
Creating Results
Section titled “Creating Results”To work within a typed error pipeline, we must wrap our synchronous operations in a Result context
at the boundaries of our system.
Wrapping throwing code with tryCatch
Section titled “Wrapping throwing code with tryCatch”Most JavaScript libraries and built-in runtime APIs (like JSON.parse or filesystem operations)
rely on exceptions. Result.tryCatch wraps these unsafe, throwing operations and converts them into
a clean Result:
The second argument is a mapper function that intercepts the thrown exception (which is of type
unknown) and converts it into your designated error type E.
Constructing from predicates with fromPredicate
Section titled “Constructing from predicates with fromPredicate”When you have a plain value and a condition that determines whether that value is valid,
Result.fromPredicate lifts the value into Result without requiring an explicit if/else block:
The second argument receives the original input, allowing you to format descriptive, context-rich error messages.
Transforming values
Section titled “Transforming values”Once our data is wrapped inside a Result, we can describe transformations on both the success and
failure branches independently.
Pure transformations with map
Section titled “Pure transformations with map”map transforms the value inside an Ok success container, leaving Err failures untouched:
If you chain multiple map steps, they will continue to execute sequentially as long as the
pipeline remains successful. The moment an Err is encountered, all subsequent map calls are
bypassed:
Pure error transformation with mapError
Section titled “Pure error transformation with mapError”Sometimes you want to standardize or translate errors before passing them upstream. mapError
transforms the error inside an Err container, leaving Ok success values untouched:
This is especially valuable at the edge of your systems, allowing you to convert low-level database or network errors into clean, user-facing error objects.
Nested pipelines with chain
Section titled “Nested pipelines with chain”When a transformation step itself can fail and returns another Result, using map would result in
a nested type: Result<E, Result<E, A>>.
To prevent this nesting, we use chain to apply the transformation and flatten the context:
If parseJson fails, the error short-circuits the pipeline immediately. If it succeeds, the
resulting userId is passed to validateUserExists. If that lookup fails, the new error is
returned. The pipeline reads as a single, linear progression of operations that can each
independently fail.
Extracting the value
Section titled “Extracting the value”Eventually, we must unpack our Result to interface with the rest of our application.
Safe defaults with getOrElse
Section titled “Safe defaults with getOrElse”getOrElse extracts the success value from an Ok, or returns a fallback value if we have an
Err:
getOrElse takes a deferred function (a thunk) to construct the fallback. This ensures that
expensive operations, like reading a default value from a config file or instantiating a fallback
cache, are only executed if a failure actually occurred.
Case analysis with match and fold
Section titled “Case analysis with match and fold”To perform distinct business logic on both the success and failure branches, you can use match for
a named object mapping, or fold for an error-first positional callback:
Side effects with tap
Section titled “Side effects with tap”When you want to perform a side effect — like logging a warning or emitting an analytics event —
without altering the data or breaking the flow, you can use tap and tapError.
tap runs its callback only on a successful Ok value:
tapError runs its callback only on an Err failure:
Both operators always return the original, unaltered Result to the next step of the pipeline.
Recovering from None
Section titled “Recovering from None”When an operation fails, you don’t always want to settle for a static default. Often, you want to
attempt an alternative strategy that could also fail. For this, we use recover:
If the primary read succeeds, the recovery lookup is bypassed. If it fails, the recovery lookup is tried.
If there are certain fatal errors that you should never attempt to recover from, you can use
recoverUnless. It takes a predicate to decide whether to let the error propagate without
attempting recovery:
Converting to and from Maybe
Section titled “Converting to and from Maybe”Result and Maybe are structurally highly compatible. The only difference is that Result
carries a typed reason for its failure, whereas Maybe models pure absence.
To discard the error context and convert to a Maybe:
Conversely, if you want to lift a Maybe into a Result, you must supply a typed error to replace
the implicit absence of a None:
Accumulating values: bind / bindTo
Section titled “Accumulating values: bind / bindTo”When you need to perform multiple sequential operations and gather their results into a single object, traditional pipelines can become deeply nested because each successive function needs access to previous results:
To solve this, we can use bindTo and bind to cleanly accumulate values key-by-key in a flat,
readable pipeline.
bindTo lifts a value into the pipeline’s accumulator object:
bind runs a new operation using the accumulated object and attaches the result to a new key:
If any step fails, the entire pipeline short-circuits and propagates the failure immediately.
Combining records: struct
Section titled “Combining records: struct”While bind is perfect for sequential steps where a latter step depends on the output of a prior
step, sometimes you have a set of independent Results that you want to combine into a single
object. For this, you can use Result.struct.
It combines a record of Results into a single Result holding a record of success values. If any
individual field is an Err, the entire struct short-circuits to that error immediately:
If multiple fields fail, Result.struct short-circuits and returns the first error encountered in
key order:
When to use Result vs try/catch
Section titled “When to use Result vs try/catch”The choice between typed results and runtime exceptions is a structural architectural decision.
Use Result when:
Section titled “Use Result when:”- The failure is expected: Validating input, parsing configurations, or lookup misses are typical domain events, not structural system failures.
- The error type matters: You want callers of a module to immediately know what can go wrong and be forced by the compiler to handle it.
- You value composition: You are building linear chains of operations using
pipewhere failures should propagate naturally.
Keep using try/catch when:
Section titled “Keep using try/catch when:”- The error is catastrophic: Database connection dropouts, out-of-memory errors, or system initialization failures represent unrecoverable states where the process should log and exit immediately.
- The boundary expects it: When interfacing with external codebases, framework routers, or
third-party libraries that rely on throwing exceptions. Convert exceptions to
Resultimmediately at these boundaries.