neverthrow

Core API

import { ok, err, Result, ResultAsync } from "neverthrow";
Function / Type Description
Result<T, E> Synchronous Result type
ResultAsync<T, E> Asynchronous Result type (wrapper around Promise<Result>)
ok(value) Constructs a success value
err(error) Constructs a failure value
.andThrough(fn) Runs a side effect; on success, returns the original value unchanged

Method Chaining

result
  .map((value) => transform(value))         // Transform the success value
  .mapErr((error) => transformErr(error))    // Transform the error value
  .andThen((value) => nextResult(value))     // Chain to the next Result (flatMap)
  .andThrough((value) => sideEffect(value))  // Run a side effect; preserve the original value on success
  .orElse((error) => recover(error))         // Recover from an error
  .match(
    (value) => handleOk(value),
    (error) => handleErr(error),
  );

Example: State-Transition Pipeline

Following Railway Oriented Programming principles, extract each expected business decision into an independent function, and compose them with method chaining. Persist only after the decision succeeds so a rejected promise remains an unexpected failure.

For the design of RequestResolver / RequestStore and how to persist state and domain events in a single transaction, see state-modeling.md#domain-events.

import { ok, err, Result } from "neverthrow";

// --- Branded Types ---

declare const RequestIdBrand: unique symbol;
type RequestId = string & { readonly [RequestIdBrand]: never };

declare const DriverIdBrand: unique symbol;
type DriverId = string & { readonly [DriverIdBrand]: never };

declare const PassengerIdBrand: unique symbol;
type PassengerId = string & { readonly [PassengerIdBrand]: never };

// --- State Types ---

type Waiting = Readonly<{
  kind: "Waiting";
  requestId: RequestId;
  passengerId: PassengerId;
}>;

type EnRoute = Readonly<{
  kind: "EnRoute";
  requestId: RequestId;
  passengerId: PassengerId;
  driverId: DriverId;
}>;

// --- Repository Types ---

type RequestResolver = Readonly<{
  findById: (id: RequestId) => Promise<Waiting | undefined>;
}>;

type RequestStore = Readonly<{
  save: (state: EnRoute) => Promise<void>;
}>;

// --- Error Types ---

type AssignDriverError =
  | Readonly<{ kind: "RequestNotFound"; requestId: RequestId }>
  | Readonly<{ kind: "DriverNotAvailable"; driverId: DriverId }>;

// --- Domain Functions ---

const ensureExists =
  (requestId: RequestId) =>
  (request: Waiting | undefined): Result<Waiting, AssignDriverError> =>
    request !== undefined
      ? ok(request)
      : err({ kind: "RequestNotFound", requestId });

const ensureDriverAvailable =
  (driverId: DriverId, isAvailable: boolean) =>
  (waiting: Waiting): Result<Waiting, AssignDriverError> =>
    isAvailable
      ? ok(waiting)
      : err({ kind: "DriverNotAvailable", driverId });

const transitionToEnRoute =
  (driverId: DriverId) =>
  (waiting: Waiting): EnRoute => ({
    kind: "EnRoute",
    requestId: waiting.requestId,
    passengerId: waiting.passengerId,
    driverId,
  });

// --- Use Case (expected-result pipeline with native async persistence) ---

const assignDriverUseCase =
  (requestResolver: RequestResolver, requestStore: RequestStore) =>
  async (
    requestId: RequestId,
    driverId: DriverId,
    isDriverAvailable: boolean,
  ): Promise<Result<EnRoute, AssignDriverError>> => {
    const request = await requestResolver.findById(requestId);
    const assignment = ok(request)
      .andThen(ensureExists(requestId))
      .andThen(ensureDriverAvailable(driverId, isDriverAvailable))
      .map(transitionToEnRoute(driverId));

    return assignment.match(
      async (enRoute) => {
        await requestStore.save(enRoute);
        return ok(enRoute);
      },
      err,
    );
  };

Recoverable External Failures

Keep a named external error in Result only when the workflow can make a recovery decision:

type PaymentAuthorizationError = {
  readonly kind: "AuthorizationTemporarilyUnavailable";
  readonly retryAfter: RetryAfter;
};

For example, the caller can defer or retry authorization after this error. It is not a wrapper for arbitrary transport failures. Likewise, when the product defines recovery for another named external failure, use a precise ExternalServiceError rather than a generic error wrapper.


This site uses Just the Docs, a documentation theme for Jekyll.