Result — Modeling Failures
1.56 kB minified + gzipped
Every function that can fail has two possible outcomes: it succeeds and returns a value, or it fails and returns an error. Yet, in traditional JavaScript and TypeScript, only the success outcome is visible in the return type. The failure is thrown out of band as a runtime exception.
The problem with exceptions
Section titled “The problem with exceptions”In standard TypeScript, exceptions are invisible to the compiler. A function signature gives no warning that an operation can fail:
This model creates three distinct failure modes:
- Invisible failure contracts: Callers must inspect source code or read documentation to know what can throw.
- Untyped error handling: In a
catch (error: unknown)block, TypeScript loses all error type information. - Unhandled crashes: Any forgot-to-catch exception crashes the entire worker or HTTP request cycle.
With Result, the possibility of failure becomes part of the return type:
The function signature Result<"INVALID_JSON" | "INVALID_PORT", UserConfig> communicates every
failure mode directly to callers. The TypeScript compiler refuses to let callers access UserConfig
without handling both error cases.
The Result approach
Section titled “The Result approach”Result<E, A> represents an operation’s outcome as a discriminated union: Ok<A> (success holding
a value) or Err<E> (failure holding a typed error).
flowchart TB
Input(["Raw Operation"]) --> Branch{"Fallible Evaluation"}
Branch -->|"Success"| Ok["Result.Ok(A)"]
Branch -->|"Failure"| Err["Result.Err(E)"]
Ok -->|"Result.map / Result.chain"| Step2["Next Transformation"]
Step2 -->|"Success"| OkFinal["Result.Ok(B)"]
Step2 -->|"New Failure"| ErrFinal["Result.Err(E2)"]
Err -->|"Short-circuit (bypassed)"| ErrFinal
OkFinal --> Boundary["Result.match / Result.getOrElse"]
ErrFinal --> Boundary
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 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 configuration object with an onError mapper that intercepts the thrown
exception (which is of type unknown) and converts it into your designated error type E.
Constructing from predicates and nullables
Section titled “Constructing from predicates and nullables”When you have a plain value and a condition that determines whether that value is valid,
Result.from.Predicate wraps the value into Result without requiring an explicit if/else block:
The second argument receives the original input, allowing you to format descriptive error messages.
When given a custom TypeScript type guard ((val: A) => val is B), Result.from.Predicate
automatically narrows the inner value from A to B.
When reading a value that may be null or undefined (such as an environment variable or header),
Result.from.nullable converts non-nullish values to Ok and missing values to Err:
Inspecting with type guards: is.ok and is.err
Section titled “Inspecting with type guards: is.ok and is.err”When interfacing with imperative code or test assertions, use Result.is.ok and Result.is.err to
narrow a Result and access .value or .error directly:
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.
Transforming success values with map
Section titled “Transforming success values with map”map transforms the value inside an Ok 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:
Transforming errors with mapError
Section titled “Transforming errors 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.
Transforming both channels with bimap
Section titled “Transforming both channels with bimap”When an operation requires mapping both the success value and the error value in a single step,
bimap accepts two callbacks: the first applies to Err, and the second applies to Ok:
Validating success values with ensure
Section titled “Validating success values with ensure”Sometimes a value is successfully parsed or retrieved, but fails a subsequent business rule.
Result.ensure tests the Ok value against a predicate. If the predicate returns false, it
converts the container to an Err using the provided error constructor:
When given a custom TypeScript type guard ((value: A) => value is B), Result.ensure
automatically narrows the success type from A to B.
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 parseSession 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.
Combining independent values with apply
Section titled “Combining independent values with apply”When you have a curried function and multiple independent Result arguments, Result.apply
evaluates and applies them one by one, short-circuiting on the first Err encountered:
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
Section titled “Case analysis with match”To perform distinct business logic on both the success and failure branches, use match:
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 errors
Section titled “Recovering from errors”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 other containers
Section titled “Converting to other containers”Converting to and from Maybe
Section titled “Converting to and from Maybe”Result and Maybe are structurally compatible. The only difference is that Result carries a
typed reason for its failure, whereas Maybe models the absence of a value.
To discard the error context and convert to a Maybe, use Result.to.Maybe:
Conversely, if you want to convert a Maybe into a Result, supply an error thunk via
Result.from.Maybe (or Maybe.to.Result) to replace None:
Swapping container contexts: sequence.Maybe
Section titled “Swapping container contexts: sequence.Maybe”When working with operations that return nested optional results (such as Result<E, Maybe<A>>),
Result.sequence.Maybe swaps the outer and inner contexts, producing Maybe<Result<E, A>>:
Converting to and from Validation
Section titled “Converting to and from Validation”While Result is designed for fail-fast error propagation, Validation accumulates all errors
across independent fields. Convert between the two representations using Result.from.Validation
and Result.to.Validation:
Accumulating values: assign / assignTo
Section titled “Accumulating values: assign / assignTo”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 assignTo and assign to cleanly accumulate values key-by-key in a flat,
readable pipeline.
assignTo wraps a value into the pipeline’s accumulator object:
assign 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 assign is ideal for sequential steps where a later 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:
Pipeline composition
Section titled “Pipeline composition”Here is an end-to-end checkout authorization pipeline that parses a raw request payload, validates inventory and spending limits, logs errors for observability, and formats an HTTP response:
Problems it solves
Section titled “Problems it solves”- Domain service error modeling: In workflows such as user authentication, checkout processing,
or inventory allocation, business failures (like expired sessions, insufficient balance, or
duplicate registrations) are expected application outcomes.
Resultmodels every failure as an explicit, typed data variant, requiring callers to handle specific failure modes without relying on untypedcatchblocks. - Untyped exception containment at system boundaries: Third-party packages, system calls, and
external parsers often throw untyped runtime exceptions that bubble up unexpectedly. Wrapping
these boundaries with
Result.tryCatchcaptures runtime exceptions immediately, converting them into typed, inspectable values before they enter application logic. - Sequential fallible pipelines: Coordinating a sequence of operations — such as reading
configuration text, parsing JSON, validating schema constraints, and generating credentials —
requires each step to succeed before running the next.
Resultcomposes these steps linearly viapipeandResult.chain, short-circuiting automatically on the first failure while preserving type safety across all intermediate values. - Error normalization across subsystem boundaries (
Result.mapError): In complex features that touch the network, database drivers, and disk storage, each subsystem produces distinct error types.Result.mapErrortranslates low-level transport errors into a cohesive domain error union at the boundary, ensuring internal business logic deals only with relevant application errors. - Combining multiple fallible steps (
Result.struct): When bootstrapping a service that requires multiple independent config entries (such as database port, API key, and host URL),Result.structcombines individualResultvalues into a verified configuration record, succeeding only if all keys resolve and short-circuiting on the first bad value.