Skip to main content

Why not neverthrow?

neverthrow pioneered Result<T, E> ergonomics in TypeScript and remains the most widely adopted library in this space. This page aims to be a fair, technical comparison for developers evaluating both libraries.

At a glance

Choose antithrow if you want:

  • Less boilerplate — auto-wrapped returns, one-step wrapping, object-form match
  • Pre-built, type-safe wrappers for standard globals (fetch, JSON.parse, atob, …)
  • A first-party ESLint plugin with multiple type-aware rules
  • An API that closely mirrors Rust's std::result
  • A Standard Schema bridge for Zod, Valibot, and ArkType

Choose neverthrow if you want:

  • The largest community and the most battle-tested option
  • Maximum Stack Overflow / blog coverage
  • Result.combineWithAllErrors — collects every error instead of short-circuiting on the first (antithrow's Result.all returns only the first Err)

Key differences

1. Less boilerplate

antithrow reduces ceremony in several places that add up in daily use.

Generator composition — chain() vs safeTry

// antithrow
import { chain } from "antithrow";

const result = chain(function* () {
const a = yield* parseNumber(inputA);
const b = yield* parseNumber(inputB);
const quotient = yield* divide(a, b);
return quotient * 2; // ← automatically wrapped in Ok
});

// neverthrow
import { safeTry, ok } from "neverthrow";

const result = safeTry(function* () {
const a = yield* parseNumber(inputA);
const b = yield* parseNumber(inputB);
const quotient = yield* divide(a, b);
return ok(quotient * 2); // ← must manually wrap in ok()
});
antithrow chain()neverthrow safeTry
Return wrappingAutomatic — just return valueManual — must return ok(value)
Unwrap syntaxyield* resultyield* result (previously required yield* result.safeUnwrap())
Async supportasync function* with yield* for both Result and ResultAsyncasync function*, but Promise<Result> requires yield* (await promise)

neverthrow has open issues around safeTry ergonomics (#581, #604) that antithrow's chain() avoids by design.

One-step wrapping — Result.try() vs fromThrowable

antithrow's Result.try() and ResultAsync.try() execute immediately and return a result. neverthrow's fromThrowable is a factory that returns a new function you then call separately:

// antithrow — one step
const parsed = Result.try(() => JSON.parse(input));
const data = await ResultAsync.try(() => fetchData());

// neverthrow — two steps each
const safeJsonParse = Result.fromThrowable(JSON.parse);
const parsed = safeJsonParse(input);

const safeFetchData = ResultAsync.fromThrowable(fetchData);
const data = await safeFetchData();

Object-form match()

antithrow uses a named-property object, which is self-documenting:

// antithrow
result.match({
ok: (value) => `Result: ${value}`,
err: (error) => `Error: ${error}`,
});

// neverthrow — positional arguments
result.match(
(value) => `Result: ${value}`,
(error) => `Error: ${error}`,
);

With positional arguments, swapping the callbacks is a silent bug. The object form makes intent explicit.

Sync-to-async bridge

Both libraries let you transition from a sync Result to a ResultAsync. antithrow uses a single toAsync() method that converts a Result into a ResultAsync, after which you chain with the full async API:

ok(2).toAsync().map(async (x) => x * 2);
ok(2).toAsync().andThen(async (x) => ok(x * 2));

neverthrow provides asyncMap and asyncAndThen as bridge methods directly on Result.

Rust-aligned naming

antithrow's API mirrors Rust's std::result closely — unwrapOr, unwrapOrElse, isOkAnd, isErrAnd, inspect, inspectErr, flatten, expect, expectErr. If you know Rust, the API is immediately familiar.

2. @antithrow/std — ready-to-use standard library wrappers

antithrow ships @antithrow/std, a companion package with non-throwing wrappers for standard globals. Each wrapper returns a Result or ResultAsync with precise error types — no unknown.

import { JSON, fetch, Response } from "@antithrow/std";

const config = JSON.parse<Config>(text);
// Result<Config, SyntaxError>

const body = await fetch("https://api.example.com/data").andThen((res) => Response.json<Data>(res));
// ResultAsync<Data, DOMException | TypeError | SyntaxError>

Covered globals: JSON.parse, JSON.stringify, fetch, Response.json/text/arrayBuffer/blob/formData, structuredClone, atob, btoa, encodeURI, decodeURI, encodeURIComponent, decodeURIComponent.

With neverthrow, you write these wrappers yourself using fromThrowable:

import { Result } from "neverthrow";

const safeJsonParse = Result.fromThrowable(JSON.parse, (e) => e as SyntaxError);
// You must create and maintain this for every throwing API you use.

3. First-party, actively maintained ESLint plugin

@antithrow/eslint-plugin provides three type-aware rules:

RuleSeverityWhat it catches
no-unused-resulterrorDiscarded Result / ResultAsync values
no-unsafe-unwrapwarnunwrap() / expect() without prior narrowing
no-throwing-callwarnThrowing APIs that have @antithrow/std replacements

neverthrow's ESLint plugin (eslint-plugin-neverthrow) has a single rule (must-use-result), is a third-party package, and has not been updated since November 2021.

4. Standard Schema support

@antithrow/standard-schema wraps any Standard Schema–conforming validator (Zod, Valibot, ArkType) into Result types:

import { validate } from "@antithrow/standard-schema";
import { z } from "zod";

const result = await validate(z.string().email(), input);
// ResultAsync<string, FailureResult>

neverthrow has no equivalent.


Common neverthrow issues

The neverthrow issue tracker surfaces recurring pain points. The table below summarizes each and whether antithrow is affected.

IssueneverthrowantithrowNotes
safeTry error type inference across multiple yields (#603)AffectedAlso affectedUnderlying TypeScript limitation — generators cannot infer a union of yielded error types across multiple yield* statements. Both libraries require an explicit type annotation on the generator when error types differ.
andThen fails on union return types (#629, #417)AffectedFixedantithrow uses InferOk/InferErr helper types on the this parameter of andThen, so union result types are inferred correctly.
combine() broken for non-tuple arrays (#434)AffectedFixedResult.all() / ResultAsync.all() provides correct type inference for both tuples and homogeneous arrays.
Poor async/await interop (#340, #514, #608)AffectedImprovedchain(async function*() {...}) seamlessly handles both Result and ResultAsync via yield*. toAsync() converts a sync Result to ResultAsync for async chaining.
ESLint plugin unmaintained (#625)AffectedFixedantithrow ships a first-party @antithrow/eslint-plugin with three type-aware rules, maintained in the same monorepo.
No toJSON / serialization (#628)AffectedAlso affectedNeither library provides built-in serialization. Results must be unwrapped before serializing.
No "finally" functionality (#525)AffectedAlso affectedWorkaround: chain .inspect() and .inspectErr() to run side-effects regardless of variant.
Unsafe internals / any usage (#648)AffectedFixedantithrow compiles with strict: true throughout. Internal casts are documented phantom-type casts only — no any.
Project maintenance concerns (#670, #531)AffectedN/Aantithrow is actively maintained with all packages in a single monorepo.
No tree shaking (#660)AffectedFixedantithrow ships ESM with no side-effects, enabling full tree shaking.
No unwrapOrElse (#587, #657)AffectedFixedantithrow provides unwrapOrElse on both Result and ResultAsync.
ok() without arguments (#595, #97)AffectedFixedantithrow supports ok() with no arguments via an explicit ok<E>(): Ok<void, E> overload.

API comparison table

Conceptantithrowneverthrow
Create Ok / Errok(value), err(error)ok(value), err(error)
Wrap throwing fnResult.try(fn)Result.fromThrowable(fn)(args)
Wrap async throwing fnResultAsync.try(fn)ResultAsync.fromThrowable(fn)(args)
Generator compositionchain(function* () { yield* result })safeTry(function* () { yield* result.safeUnwrap() })
Pattern matchmatch({ ok, err })match(okFn, errFn)
Combine resultsResult.all(list)Result.combine(list)
Standard library wrappers@antithrow/std
Node.js API wrappers@antithrow/node
Schema validation bridge@antithrow/standard-schema
ESLint rules3 first-party rules1 third-party rule
Dependencies00

"But neverthrow has way more users"

It does — and that's a reasonable factor in any evaluation. neverthrow has 1M+ weekly npm downloads, 7k+ GitHub stars, and years of production use across many teams.

If community size and volume of existing resources are your top priorities, neverthrow is a safe choice.

antithrow prioritizes a different set of trade-offs:

  • Zero dependencies and a small, stable API surface — less to audit, less to break.
  • Incremental adoption — both libraries implement the same Ok/Err conceptual model, so migration is straightforward. Start at your API boundaries and work inward.
  • First-party tooling — the ESLint plugin, standard library wrappers, and schema bridge are maintained in the same monorepo as the core, so they stay in sync.

Maintenance notes

As of February 2026:

antithrow is actively maintained with all packages — core, @antithrow/std, @antithrow/node, @antithrow/eslint-plugin, and @antithrow/standard-schema — developed and released from a single monorepo.


Next steps