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

@shirudo/result

v1.1.1

Published

Robust, type-safe error handling for TypeScript using the Result pattern (Monad)

Readme

@shirudo/result

A Result<T, E> type for TypeScript. Functions return their failures instead of throwing them.

function loadUser(id: string): Result<User, UserError>

The failure is part of the signature: callers see exactly what can go wrong, the compiler insists both cases are handled, and value/error are only accessible after narrowing. What used to be a forgotten catch is now a type error.

The rest of the package is tooling around that one type: pipe operators, async variants, generator-based do-notation, exhaustive error matching, and collection helpers. The library has zero dependencies, ships as ESM and CJS, and runs on Node 20+ and edge runtimes.

CI npm version License: MIT

Installation

npm install @shirudo/result
pnpm add @shirudo/result
yarn add @shirudo/result

Quick Start

import { err, ok, type Result } from '@shirudo/result';
import { map, match } from '@shirudo/result/operators';

type User = { id: string; email: string; active: boolean };
type UserError =
    | { code: 'not-found'; id: string }
    | { code: 'inactive'; id: string };

const users = new Map<string, User>([
    ['1', { id: '1', email: '[email protected]', active: true }],
]);

function loadUser(id: string): Result<User, UserError> {
    const user = users.get(id);
    if (!user) return err({ code: 'not-found', id });
    if (!user.active) return err({ code: 'inactive', id });
    return ok(user);
}

// Narrow explicitly:
const result = loadUser('1');
if (result.isOk()) {
    console.log(result.value.email); // `value` is only accessible in this branch
}

// Or compose and resolve in one expression. `map` only runs on Ok,
// and the error keeps its type all the way to `match`:
const message = loadUser('1').pipe(
    map(user => `Welcome back, ${user.email}`),
    match({
        ok: greeting => greeting,
        err: error =>
            error.code === 'not-found'
                ? `No user with id ${error.id}`
                : `User ${error.id} is deactivated`,
    }),
);

Why Result Instead of try/catch?

TypeScript cannot type a catch block: every thrown value arrives as unknown, and nothing in a function's signature reveals that it throws at all. Callers either remember to catch, or they find out in production.

A Result<User, UserError> puts the failure into the signature. The compiler forces both states to be handled, narrows value and error access to the matching state (as in the Quick Start above), and keeps the error type intact across every transformation.

Why This Library?

There are several Result implementations for TypeScript. This one is built around a few hard guarantees:

  • Error types that cannot lie. Declaring an explicit error type requires an error mapper: fromPromise<User, ApiError>(promise) without one is a compile error, so E never silently holds an unmapped unknown. And bugs inside the mapper itself are rethrown instead of being disguised as Err values.
  • Exhaustive matching, checked at compile time. matchError().when(NotFoundError, ...).run() only compiles once every error case is handled, and matchTag does the same for discriminated unions. Add a new error variant, and every unhandled match site turns red.
  • Four interchangeable styles, one type. Explicit isOk()/isErr() checks, pipe/pipeAsync operator chains, generator-based do-notation (task), and builder-based error matching all work on the same immutable, frozen Result. Use whichever style fits each call site.
  • Safe at runtime boundaries. isResult() validates an internal brand plus payload shape instead of accepting lookalike objects, and toSerialized()/fromSerialized() round-trip Results through JSON without ambiguity.
  • Zero dependencies, runs anywhere. The package ships ESM and CJS builds with tree-shakeable subpath exports, and it runs on Node 20+ and in edge runtimes.
  • Verified, not promised. Every TypeScript snippet in this README and the docs is compile-checked in CI, the API is covered by 400+ runtime tests plus compile-time type tests, and the package exports are verified for ESM, CJS, and TypeScript consumers.

When Not to Use It

  • If your codebase is already built on Effect, you do not need this package. Its Either/Exit types come with the surrounding ecosystem.
  • For short scripts and prototypes, plain try/catch is often the simpler tool. The value of typed errors grows with the number of call sites that must handle them.
  • Keep throwing for programmer errors. Broken invariants and failed assertions should crash loudly; Result is for failures the caller is expected to handle.

Common Workflows

Catch throwing APIs

import { Result } from '@shirudo/result';

const parseJson = Result.fromThrowable(
    JSON.parse,
    error => ({ code: 'parse' as const, cause: error }),
);

const parsed = parseJson('{"valid": true}');

const response = await Result.fromPromise(
    Promise.resolve({ ok: true }),
    error => ({ code: 'network' as const, cause: error }),
);

Compose with pipe operators

import { Result } from '@shirudo/result';
import { filter, map, mapErr } from '@shirudo/result/operators';

const processed = Result.ok<number, string>(10).pipe(
    map(value => value * 2),
    filter(
        value => value > 25,
        () => 'too small',
    ),
    mapErr(error => `Validation failed: ${error}`),
);

Compose async work

import { Result } from '@shirudo/result';
import { mapAsync, tryMapAsync } from '@shirudo/result/operators';

const db = {
    async getUser(id: number) {
        return { id, email: '[email protected]' };
    },
};

const normalizedEmail = await Result.ok<number, string>(1).pipeAsync(
    mapAsync(async id => db.getUser(id)),
    tryMapAsync(async user => user.email.toLowerCase()),
);

Use task notation for sequential flows

import { Result, task } from '@shirudo/result';

function findUser(id: string) {
    return Result.ok({ id, email: '[email protected]' });
}

function ensureEmail(user: { id: string; email?: string }) {
    return user.email
        ? Result.ok(user.email)
        : Result.err({ code: 'missing-email' as const, id: user.id });
}

const emailResult = await task(function* () {
    const user = yield* findUser('1');
    return yield* ensureEmail(user);
});

Handle error classes exhaustively

import { Result } from '@shirudo/result';

class NotFoundError extends Error {
    readonly code = 'not-found';
}
class RateLimitError extends Error {
    readonly code = 'rate-limited';
    constructor(readonly retryAfter: number) {
        super(`rate limited, retry in ${retryAfter}s`);
    }
}

const result: Result<string, NotFoundError | RateLimitError> = Result.err(new RateLimitError(30));

if (result.isErr()) {
    const message = result
        .matchError()
        .when(NotFoundError, () => 'No such record')
        .when(RateLimitError, error => `Retry in ${error.retryAfter}s`)
        .run(); // run() only compiles because every error class is handled
}

Match discriminated-union errors

import { Result, matchTag } from '@shirudo/result';

type DomainError =
    | { code: 'network'; retryAfter: number }
    | { code: 'validation'; field: string };

const failed = Result.err<DomainError>({ code: 'network', retryAfter: 30 });

const message = matchTag(failed, 'code', {
    network: error => `Retry in ${error.retryAfter}s`,
    validation: error => `Invalid field: ${error.field}`,
});

Imports

Everything is available from the package root:

import { Result, err, ok, task } from '@shirudo/result';

Focused subpath exports are available for clearer imports and package-level checks:

import { UnwrapOnErrError } from '@shirudo/result/errors';
import { flatMapAsync, map } from '@shirudo/result/operators';
import { sequence, sequenceRecord } from '@shirudo/result/collections';

Documentation

The full documentation lives in docs/ and is built with VitePress.

Development

pnpm install
pnpm check
pnpm check:clean

Useful focused commands:

pnpm docs:dev
pnpm docs:check
pnpm test:exports

License

MIT