npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zireal/result-kit

v5.0.2

Published

Type-safe result and structured error utilities for TypeScript. Zero-dependency, ESM-only.

Readme

@zireal/result-kit

Type-safe Result handling for TypeScript. Model failure as a value instead of throwing through your service layer.

  • Plain data. A Result is { ok: true, value } or { ok: false, error } — no class, no methods, no hidden brand. It survives JSON.stringify and crosses process boundaries intact.
  • Zero dependencies. No runtime dependencies, no peer dependencies.
  • Two surfaces, one implementation. A fluent wrapper for ergonomics, and a free-function core for bundle size. The wrapper delegates to the core; it is not a second codebase.
  • Genuinely tree-shakable. Import three functions and ship three functions. The fluent wrapper lives behind a separate entrypoint, so it costs nothing unless you import it.
import { ok } from '@zireal/result-kit/fluent';

const greeting = ok(user)
  .map((u) => u.name)
  .match({ ok: (name) => `Hello, ${name}`, err: () => 'Hello, stranger' });

Upgrading from 1.x? See MIGRATION.md. It is a full rework — most names moved, and one of them (unwrapOrThrow) breaks silently.


Installation

pnpm add @zireal/result-kit

| Requirement | Version | |---|---| | Node | >=22.12 | | TypeScript | >=6.0 | | Module format | ESM only — no CJS build |

moduleResolution must be "bundler", "node16", or "nodenext". On CommonJS, load it with require('@zireal/result-kit') (Node 22.12+ supports requiring ESM) or await import(...).


The two surfaces

Both are first-class and fully supported. Pick per project, or mix per file.

| | @zireal/result-kit/fluent | @zireal/result-kit | |---|---|---| | Style | chained methods | free functions | | Reads like | ok(x).map(f).unwrapOr(0) | unwrapOr(map(ok(x), f), 0) | | Best for | application code, linear pipelines | libraries, hot paths, minimal bundles | | Bundle cost | the wrapper class | only the functions you import |

Start with /fluent. It is the more comfortable surface and what most application code should use. Reach for the core when bundle size matters, or when you are writing a library and would rather not impose a wrapper on your callers.

The core is self-sufficient — it never needs /fluent. That is the point of the split, and it is something a class-based library structurally cannot offer, because there the methods and the data are the same object.


Quick start

The fluent surface

import { from } from '@zireal/result-kit/fluent';

const label = from(findUser('u1'))
  .map((user) => user.name)
  .mapErr((e) => e.message)
  .unwrapOr('anonymous');

from(...) lifts a plain Result into the wrapper; .toResult() takes you back out. The plain union is the source of truth — the wrapper is a transient envelope for the duration of a chain, not something to store or serialize.

The core surface

The same thing, without the wrapper:

import { map, mapErr, unwrapOr } from '@zireal/result-kit';

const named = map(findUser('u1'), (user) => user.name);
const label = unwrapOr(mapErr(named, (e) => e.message), 'anonymous');

Read inside-out rather than left-to-right. If that nesting bothers you, that is exactly what /fluent and safeTry are for.

Producing a Result

import { ok, err } from '@zireal/result-kit';
import type { Result } from '@zireal/result-kit';

function findUser(id: string): Result<User, NotFound> {
  const user = db.get(id);

  return user ? ok(user) : err({ type: 'not_found', message: `No user ${id}` });
}

Async

One await at the front, a terminal at the end, no ceremony in between:

import { ResultAsync } from '@zireal/result-kit/fluent';

const name = await ResultAsync.from(loadUser(id))
  .andThen(requireActive)
  .map((user) => user.name)
  .match({ ok: (n) => n, err: () => 'anonymous' });

ResultAsync implements PromiseLike, so await resultAsync gives you the plain Result — awaiting is the sanctioned way out. It also means a floating un-awaited chain is caught by the standard no-floating-promises lint rule, for free.

In the core, async is just Promise<Result<T, E>>. There is no new type, and no Async-suffixed twin of anything — the transforms take a value or a promise in the same signature:

const upper = await map(loadUser(id), (user) => user.name.toUpperCase());

Core concepts

Result<T, E>

type Result<T, E> = Ok<T> | Err<E>;

interface Ok<T>  { readonly ok: true;  readonly value: T }
interface Err<E> { readonly ok: false; readonly error: E }

Purely structural — there is no brand. Any { ok: true, value } is an Ok<T>, whoever built it. That is a deliberate guarantee rather than an accident: it is what lets a Result round-trip through JSON, cross an HTTP boundary, or come back from a worker and still be a Result.

const parsed = JSON.parse(JSON.stringify(result)); // still a usable Result

Three caveats on that round-trip:

  • A cause may hold something non-serializable.
  • Exit the fluent wrapper first — serialize chain.toResult(), not the chain.
  • ok() with no argument does not survive it. The value is { ok: true, value: undefined } — two fields, as always — but JSON.stringify omits an undefined property, so it round-trips to { ok: true } and the value key is gone, not undefined. Code doing 'value' in parsed will be surprised; parsed.value still reads undefined and is usually fine.

Narrowing

isOk / isErr are type predicates, so the field access after them is checked:

import { isOk } from '@zireal/result-kit';

if (isOk(result)) {
  result.value;   // T
} else {
  result.error;   // E
}

On the fluent side, .isOk() / .isErr() return plain booleans and buy you no narrowing — a method cannot emit a predicate about its own class's generics. Narrow with .match() or a terminal instead, or leave the wrapper with .toResult() first.


API

Root — @zireal/result-kit

Constructors and guards

| | | |---|---| | ok(value) / ok() | Build an Ok. The no-arg form is for Result<void, E> | | err(error) | Build an Err | | isOk(r) / isErr(r) | Type-predicate guards | | isTypedError(e) | Whether a value follows the TypedError convention | | defineError(type, message) | Build a typed-error constructor — see below |

Transforms — each takes a Result or a Promise<Result>

| | | |---|---| | map(r, fn) | Transform the value; pass Err through | | mapErr(r, fn) | Transform the error; pass Ok through | | andThen(r, fn) | Chain a fallible step; accumulates the error channel to E \| F | | orElse(r, fn) | Recover from an error | | inspect(r, fn) / inspectErr(r, fn) | Tee one side for a side effect; returns the input unchanged |

Terminals — these leave the Result world

| | | |---|---| | match(r, { ok, err }) | Collapse both branches to one value. Exhaustive by construction | | unwrapOr(r, default) | The value, or a fallback | | unwrapOrElse(r, fn) | The value, or a fallback computed from the error | | unwrapOrThrow(r, message?) | The value, or throw a real Error with the original in cause | | toNullable(r) | The value, or null |

Collections

| | | |---|---| | combine(results) | All-or-nothing; preserves the tuple type. First Err wins | | combineWithAllErrors(results) | Same, but collects every error into an array | | partition(results) | Split into [values, errors] — both halves, always |

Formatters — presentation over the TypedError[] that combineWithAllErrors accumulates

| | | |---|---| | groupByType(errors) | Group by the type discriminant; each group keeps its narrowed variant | | prettifyErrors(errors) | One ✖ type: message line per error |

Interop

| | | |---|---| | fromNullable(value, error) | null / undefined becomes an Err | | fromPredicate(value, pred, error) | Narrows T when pred is a type guard | | fromThrowable(fn, onThrow) | Wrap a throwing function into a Result-returning one | | fromPromise(promise, onReject) | Catch a rejection into the error channel | | fromThrowableAsync(fn, onReject) | The lazy, reusable form of fromPromise |

Do-notationsafeTry, safeUnwrap. See below.

TypesResult Ok Err TypedError ErrorCtor OkTypeOf ErrTypeOf

/fluent@zireal/result-kit/fluent

Exports ok err from safeTry fromPromise fromThrowableAsync ResultAsync, plus the ResultChain type.

ok / err / safeTry / fromPromise / fromThrowableAsync exist at both entrypoints under the same names — the root's return plain data, these return wrappers. That is deliberate: you should not have to learn two vocabularies.

ResultChain<T, E> mirrors the core one-to-one: .map() .mapErr() .andThen() .orElse() .inspect() .inspectErr() .match() .unwrapOr() .unwrapOrElse() .unwrapOrThrow() .toNullable() .isOk() .isErr() .toResult() .toAsync().

ResultAsync<T, E> is ResultChain lifted — every value-terminal returns a Promise. Two deliberate differences: there is no .isOk() / .isErr() (an always-truthy if (ra.isOk()) is a footgun, and narrowing needs the plain union anyway), and .toJSON() throws rather than silently serializing {} for a value that has not arrived yet.

Array-shaped helpers stay root-onlycombine, combineWithAllErrors, partition, and the three sync constructors fromNullable / fromPredicate / fromThrowable. They take arrays or non-Result inputs, so there is no single instance for a method to hang off. Re-enter with from(...):

from(combine([a, b])).map(sum).unwrapOr(0);

Crossing from sync to async is explicit, via .toAsync():

ok(user).map(validate).toAsync().andThen(saveRemote).match({
  ok: (saved) => saved.id,
  err: (e) => e.message,
});

It is explicit on purpose. A settled Result cannot promise an asynchronous output — a transform that short-circuits never runs its callback at all, so on the Err branch there would be nothing to await.


Do-notation

For flows where chaining gets awkward — branches, early exits, or a step needing a value from two steps back. Any Err exits the whole block:

import { ok, safeTry, safeUnwrap } from '@zireal/result-kit';

const total = safeTry(function* () {
  const user = yield* safeUnwrap(findUser(id));      // an Err here short-circuits
  const order = yield* safeUnwrap(loadOrder(user));  // each binds its own type

  return ok(user.credit + order.total);              // return a Result explicitly
});

The error channel accumulates — that block is Result<number, NotFound | OrderMissing>.

It works with promises too. Inside an async function*, a Promise<Result> unwraps with no await:

const total = await safeTry(async function* () {
  const user = yield* safeUnwrap(fetchUser(id));

  return ok(user.credit);
});

On /fluent there is no safeUnwrap — the wrappers are self-iterable, so you yield* them directly, and the block hands back a wrapper so the chain continues:

import { ok, safeTry, from } from '@zireal/result-kit/fluent';

const total = safeTry(function* () {
  const user = yield* from(findUser(id));

  return ok(user.credit);
}).unwrapOr(0);

Structured errors

E is fully generic — a Result's error can be a string, an Error, or anything else. TypedError is an opt-in convention for when you want errors you can narrow on:

import { defineError, err } from '@zireal/result-kit';

const notFound = defineError('not_found', (d: { id: string }) => `No user ${d.id}`);
const forbidden = defineError('forbidden', 'Not permitted');

type AppError = ReturnType<typeof notFound> | ReturnType<typeof forbidden>;

const failure = err(notFound({ id: 'u1' }));
//    ^? Err<TypedError<'not_found', { id: string }>>

The values are plain objects — { type, message, details?, cause? } — never classes, never extends Error. They serialize, and they narrow:

switch (error.type) {
  case 'not_found': return error.details?.id;
  case 'forbidden': return null;
}

Each constructor also carries .type, readable without building a value, and a .is() guard for narrowing a union at runtime.

Presenting accumulated errors

combineWithAllErrors collects every failure rather than stopping at the first, which is the shape you want for form validation or a batch job. Two helpers turn that array into something you can use:

import { combineWithAllErrors, groupByType, prettifyErrors } from '@zireal/result-kit';

const combined = combineWithAllErrors([checkName(input), checkAge(input), checkEmail(input)]);

if (!combined.ok) {
  console.error(prettifyErrors(combined.error));
  // ✖ too_short: Name must be at least 2 characters
  // ✖ out_of_range: Age must be between 13 and 120

  const groups = groupByType(combined.error);
  groups.too_short?.forEach((e) => highlight(e.details?.field));
  //     ^? TooShort[] — the variant's own `details`, not the union's
}

groupByType's keys are optional, because a variant that did not occur has no key — groups.out_of_range is OutOfRange[] | undefined. That is deliberate: typing an absent group as present would hand you undefined under a type promising an array.

prettifyErrors reads only type and message, never details. That is not a redaction guarantee, though: a variant whose message is computed from its payload ((d) => \No user ${d.id}`) has already put that data in message. Keep anything sensitive out of message` — no formatter can take it back out.


Tree-shaking

The root entrypoint is a flat barrel of standalone functions and the package is marked sideEffects: false. Import map and you ship map.

The fluent wrapper lives behind /fluent and is never reachable from the root bundle. That boundary is enforced by an automated test which inspects the built output — not by convention, and not by review. If you never import /fluent, no wrapper code reaches your bundle.


Documentation

License

MIT © Ali Farooq