Skip to content

neverthrow

Developer UtilitiesUtilityTypeScript

What it is

neverthrow provides a Result type for TypeScript, making failure part of a function's return type instead of an invisible exception.

Functions return Ok or Err rather than throwing. The compiler then forces callers to acknowledge the failure case before reaching the value.

Installation

npm install neverthrow

Getting started

The smallest useful thing you can do with it, and what each part means.

Failure in the type signature
import { ok, err, Result } from 'neverthrow';

type ParseError = { kind: 'empty' } | { kind: 'not_a_number'; input: string };

function parseAge(input: string): Result<number, ParseError> {
  if (!input.trim()) return err({ kind: 'empty' });
  const value = Number(input);
  if (Number.isNaN(value)) return err({ kind: 'not_a_number', input });
  return ok(value);
}

const result = parseAge(raw);

// You cannot reach the value without handling the error case.
if (result.isErr()) {
  return respond(400, result.error);
}
console.log(result.value);
The signature now documents exactly how this can fail. Compare with a throwing version, where the caller has no way to know from the type alone.
Chaining without nesting
const outcome = parseAge(raw)
  .map((age) => age + 1)                       // runs only on success
  .andThen((age) => age >= 18 ? ok(age) : err({ kind: 'too_young' } as const))
  .mapErr((e) => ({ ...e, field: 'age' }));    // enrich the error

outcome.match(
  (age) => respond(200, { age }),
  (error) => respond(400, error),
);
map transforms a success, andThen chains another fallible step, and mapErr adjusts the failure. Nothing runs after the first Err, so there is no nesting of if-statements.

Advanced usage

Where the library earns its place over a simpler alternative.

Wrapping throwing APIs
import { ResultAsync, fromThrowable } from 'neverthrow';

// Convert a throwing function into one that returns Result.
const safeJsonParse = fromThrowable(JSON.parse, (e) => ({
  kind: 'invalid_json' as const,
  cause: e,
}));

// Convert a promise into a ResultAsync.
const fetchUser = (id: string) =>
  ResultAsync.fromPromise(
    fetch(`/api/users/${id}`).then((r) => r.json()),
    (e) => ({ kind: 'network' as const, cause: e }),
  );

const user = await fetchUser('42')
  .andThen((raw) => safeJsonParse(raw))
  .unwrapOr(null);
The realistic pattern is a boundary: wrap third-party throwing code once, then work with Results internally where the compiler can check your handling.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Property value does not exist on Result
Narrow first with isOk()/isErr(), or use match/unwrapOr. Direct access is deliberately blocked.
Mixing Result and thrown errors
Pick one convention per layer. A half-converted codebase is worse than either approach alone.

Best practices

  • Use Result for expected failures — validation, not-found, parse errors — and keep exceptions for genuine bugs.
  • Wrap throwing libraries at the boundary with fromThrowable rather than scattering try/catch.
  • Give errors a discriminated union type so callers can match on the kind.
  • Avoid _unsafeUnwrap outside tests; it defeats the purpose.

Background

Why it exists, and what it was reacting to.

Inspired by Rust's Result, neverthrow addresses a real weakness in TypeScript: a function's signature says nothing about what it can throw, so error handling is invisible to both the compiler and the reader.