mrkeyoor.com_
Wed 23 Sept 00:35 UTC
npmUtilsupdated 22 Sept 2026

better-result review

better-result 3.0.1 is a TypeScript Result library for functions that can return either an Ok value or a typed Err. It includes tagged Error subclasses, generator composition, Promise rejection capture, retry controls, collection helpers, and schema-checked serialization. The 3.0 line separates expected failures from defects by turning unexpected callback throws into Panic. Version 3.0.1 fixes inferred error unions when andThen or tryRecover callbacks introduce another error type. Our fresh install found no dependencies, bundled declarations, working import and require paths on Node 22, and a 3.3 KB gzipped browser bundle.

Verdict

better-result 3.0.1 installed in 0.9 seconds with 0 dependencies, used 1 MB on disk, and produced a 3.3 KB gzipped bundle in our sandbox. It fits TypeScript 5.4+ teams that will model expected failures consistently; teams wanting only a two-variant container should pick a narrower Result package.

We installed it

Lab card: what happened when we installed better-resultScreenshot of better-result documentation
Install✓ · 0.9s1 package on disk · 1 MB
ImportESM import works · require() works · ESM package with exports map
Browser3.3 KBgzipped (10.5 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does better-result install cleanly?

Yes. In a fresh container with an empty cache, npm install better-result finished in 0.9s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does better-result add to a browser bundle?

3.3 KB gzipped (10.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does better-result work with both ESM and CommonJS?

Yes. Both import 'better-result' and require('better-result') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does better-result include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

better-result or neverthrow: which should you use?

neverthrow: Choose it for an established Result and ResultAsync API with many existing integrations and examples. better-result 3.0.1 installed in 0.9 seconds with 0 dependencies, used 1 MB on disk, and produced a 3.3 KB gzipped bundle in our sandbox.

When should you not use better-result?

Your project is pinned below TypeScript 5.4. The README names 5.4 as the minimum for the current 3.x API.

API stability3/5Version 3.0.1 keeps the 3.0 Ok, Err, Result.gen, Result.await, TaggedError, Panic, retry, and codec design, and its only listed change fixes callback error-lane inference in andThen and tryRecover. The major line is still new: 3.0 changed TaggedError subclass syntax, reserved match on tagged errors, and removed three older serialization entry points. A detailed 2.x migration page exists, but an upgrade still touches source code and boundary schemas.
Docs5/5The README begins with the expected-error versus defect rule, then shows inferred types for sync generators, async generators, recovery, observers, retries, collections, codecs, and Panic handling. It explicitly says that fetch HTTP errors do not reject, that retry times excludes the first attempt, and that cancellation requires forwarding the signal. The linked site has API, migration, error, generator, collection, and serialization pages, and the root URL returned HTTP 200 in our check.
Maintenance5/5The 3.0.1 release was published on August 11, 2026 to correct inferred error unions contributed through pull request 109. GitHub reports a push on August 23, 2026, 1,929 stars, and 4 open issues and pull requests combined; the repository is not archived. Those dates show current work after the major release. The short project history still gives less evidence about long-term compatibility than older Result libraries provide.
Ecosystem4/5npm counted 5,757,088 downloads for the week ending August 24, 2026, while GitHub reports 1,929 stars. The package bundles TypeScript declarations, has no runtime or peer dependencies, and accepts any validator that implements Standard Schema for its codec boundary. Its ESM declaration and TypeScript 5.4 floor narrow compatibility with older projects, and the surrounding tutorials and adapters remain less numerous than neverthrow's.

Use it if

  • Your TypeScript service needs validation, missing-record, authorization, or upstream failures represented in return types rather than hidden behind throws.
  • Several Result-returning steps must read like ordinary sequential code while preserving the full error union.
  • Domain errors need real Error subclasses, literal tags, typed payloads, JSON output, and exhaustive matching.
  • Results cross queues, RPC calls, or storage boundaries where Standard Schema validation can check both success and error payloads.
Skip it if

Setup reality

We installed better-result 3.0.1 in a fresh Node 22 Bookworm sandbox. npm finished in 0.9 seconds and left 1 package using 1 MB. The package has 0 direct and 0 peer dependencies, occupies 284 KB unpacked, bundles TypeScript declarations, and produced a 10.5 KB minified browser bundle, 3.3 KB gzipped. npm audit reported 0 known vulnerabilities.

The package declares ESM through type module and has an exports map. Both ESM import and require worked on our Node 22 box, but the README still describes the package as ESM-only. Treat import as the supported path and test older CommonJS build chains before adopting it. TypeScript 5.4 is the documented floor. There is no credentials file, code generation step, native build, or peer package to configure.

The setup cost moves into error design. Expected failures belong in Err; throws inside mapping, matching, observers, generators, or codec validation become Panic. Catch Panic at an application supervision boundary instead of adding it to every routine error handler. Async generator workflows must wrap each Promise with Result.await. Result.tryPromise catches rejection, while a fulfilled fetch response with status 404 or 500 still needs an explicit response.ok check.

Retry configuration counts attempts after the initial call, so times: 3 permits 4 calls. Cancellation only reaches the operation when you pass the supplied AbortSignal into fetch or another cancellation-aware API. Result.allAsync starts inputs concurrently but reports the first error by input order. Result.codec also requires four Standard Schema-compatible boundary schemas; the package does not choose or install a validator for you.

Patterns

Return an explicit parse failure create-result

import { Result } from 'better-result';

function parsePort(input: string) {
  const port = Number(input);
  return Number.isInteger(port) && port > 0
    ? Result.ok(port)
    : Result.err('invalid-port' as const);
}

The return type retains both number and the literal invalid-port error, so callers must handle the failed parse.

Leave Result with one output type match-branches

const label = parsePort(input).match({
  ok: (port) => `Listening on ${port}`,
  err: (reason) => `Cannot start: ${reason}`,
});

match requires policies for Ok and Err and produces one ordinary value after both branches agree on an output.

Create a typed Error variant define-tagged-error

import { TaggedError } from 'better-result';

class UserMissing extends TaggedError('UserMissing')<{
  userId: string;
  message: string;
}> {}

const error = new UserMissing({
  userId: 'u_42',
  message: 'User does not exist',
});

Version 3 uses the class form shown here with no trailing factory call after the property type.

Stop a workflow at the first Err compose-sync

const checkout = (cartId: string) =>
  Result.gen(function* () {
    const cart = yield* findCart(cartId);
    const reservation = yield* reserveStock(cart.items);
    const receipt = yield* chargePayment(cart, reservation);
    return Result.ok(receipt);
  });

Result.gen unwraps each Ok and returns the first Err while TypeScript unions errors from every yielded operation.

Await Result promises inside a generator compose-async

const dashboard = await Result.gen(async function* () {
  const session = yield* Result.await(readSession());
  const user = yield* Result.await(fetchUser(session.userId));
  return Result.ok({ session, user });
});

A Promise<Result> needs Result.await to provide the async iterator contract and preserve its error type.

Translate a rejected Promise capture-rejection

const response = Result.tryPromise({
  try: ({ signal }) => fetch(url, { signal }),
  catch: (cause) => new NetworkFailure({ cause, url, message: 'Request failed' }),
});

Result.tryPromise catches rejection only; HTTP 4xx and 5xx responses fulfill fetch and require a separate response.ok check.

Retry with cancellation and backoff retry-request

const result = Result.tryPromise(
  { try: ({ signal }) => fetch(url, { signal }), catch: toNetworkFailure },
  { signal: controller.signal, retry: { times: 3, delayMs: 100, backoff: 'exponential', jitter: true } },
);

times: 3 allows 3 retries after the first call, for at most 4 attempts; forwarding signal lets cancellation reach fetch.

Recover only a missing user recover-error

const user = findUser(userId).tryRecover((error) =>
  UserMissing.is(error) ? Result.ok(guestUser) : Result.err(error),
);

Returning unrelated errors unchanged removes only UserMissing from the resulting error union.

Require every cached value collect-all

const context = Result.all([
  loadCachedUser(userId),
  loadCachedTeam(teamId),
  loadCachedPlan(accountId),
] as const);

Result.all preserves tuple positions and stops at the first Err instead of collecting every failure.

Keep successful and failed rows partition-results

const checked = rows.map(validateImportRow);
const [validRows, rowErrors] = Result.partition(checked);

Result.partition evaluates the full input and preserves relative order within both output arrays.

Record either branch without replacing it observe-result

const observed = loadAccount(id).tapBoth({
  ok: (account) => metrics.loaded(account.id),
  err: (error) => metrics.failed(error._tag),
});

tapBoth returns the original Result; a thrown observer error is treated as a defect and becomes Panic.

Report defects at a supervision boundary handle-panic

import { Panic } from 'better-result';

try {
  runApplication();
} catch (error) {
  if (Panic.is(error)) reportDefect(error.message, error.cause);
  else throw error;
}

Panic represents an unexpected throw or broken invariant, so routine Result handlers should not recover it as a domain error.

Alternatives

PackageRegistryPick it when
neverthrownpmChoose it for an established Result and ResultAsync API with many existing integrations and examples.
oxide.tsnpmChoose it when Rust-shaped Option and Result types should come from the same small package.
ts-results-esnpmChoose it for an ESM Result and Option implementation without better-result's retry and codec layer.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.