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

better-result

v2.7.0

Published

Lightweight Result type with generator-based composition

Readme

better-result

Lightweight Result type for TypeScript with generator-based composition.

Install

New to better-result?

npx better-result init

Upgrading from v1?

npx better-result migrate

Quick Start

import { Result } from "better-result";

// Wrap throwing functions
const parsed = Result.try(() => JSON.parse(input));

// Check and use
if (Result.isOk(parsed)) {
  console.log(parsed.value);
} else {
  console.error(parsed.error);
}

// Or use pattern matching
const message = parsed.match({
  ok: (data) => `Got: ${data.name}`,
  err: (e) => `Failed: ${e.message}`,
});

Contents

Creating Results

// Success
const ok = Result.ok(42);

// Error
const err = Result.err(new Error("failed"));

// From throwing function
const result = Result.try(() => riskyOperation());

// From promise
const result = await Result.tryPromise(() => fetch(url));

// With custom error handling
const result = Result.try({
  try: () => JSON.parse(input),
  catch: (e) => new ParseError(e),
});

Transforming Results

const result = Result.ok(2)
  .map((x) => x * 2) // Ok(4)
  .andThen(
    (
      x, // Chain Result-returning functions
    ) => (x > 0 ? Result.ok(x) : Result.err("negative")),
  );

// Standalone functions (data-first or data-last)
Result.map(result, (x) => x + 1);
Result.map((x) => x + 1)(result); // Pipeable

Handling Errors

// Transform error type
const result = fetchUser(id).mapError((e) => new AppError(`Failed to fetch user: ${e.message}`));

// Recover from specific errors
const result = fetchUser(id).match({
  ok: (user) => Result.ok(user),
  err: (e) => (e._tag === "NotFoundError" ? Result.ok(defaultUser) : Result.err(e)),
});

Extracting Values

// Unwrap (throws on Err)
const value = result.unwrap();
const value = result.unwrap("custom error message");

// With fallback
const value = result.unwrapOr(defaultValue);

// Pattern match
const value = result.match({
  ok: (v) => v,
  err: (e) => fallback,
});

Generator Composition

Chain multiple Results without nested callbacks or early returns:

const result = Result.gen(function* () {
  const a = yield* parseNumber(inputA); // Unwraps or short-circuits
  const b = yield* parseNumber(inputB);
  const c = yield* divide(a, b);
  return Result.ok(c);
});
// Result<number, ParseError | DivisionError>

Async version with Result.await:

const result = await Result.gen(async function* () {
  const user = yield* Result.await(fetchUser(id));
  const posts = yield* Result.await(fetchPosts(user.id));
  return Result.ok({ user, posts });
});

Errors from all yielded Results are automatically collected into the final error union type.

Normalizing Error Types

Use mapError on the output of Result.gen() to unify multiple error types into a single type:

class ParseError extends TaggedError("ParseError")<{ message: string }>() {}
class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {}
class AppError extends TaggedError("AppError")<{ source: string; message: string }>() {}

const result = Result.gen(function* () {
  const parsed = yield* parseInput(input); // Err: ParseError
  const valid = yield* validate(parsed); // Err: ValidationError
  return Result.ok(valid);
}).mapError((e): AppError => new AppError({ source: e._tag, message: e.message }));
// Result<ValidatedData, AppError> - error union normalized to single type

Retry Support

const result = await Result.tryPromise(() => fetch(url), {
  retry: {
    times: 3,
    delayMs: 100,
    backoff: "exponential", // or "linear" | "constant"
  },
});

Conditional Retry

Retry only for specific error types using shouldRetry:

class NetworkError extends TaggedError("NetworkError")<{ message: string }>() {}
class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {}

const result = await Result.tryPromise(
  {
    try: () => fetchData(url),
    catch: (e) =>
      e instanceof TypeError // Network failures often throw TypeError
        ? new NetworkError({ message: (e as Error).message })
        : new ValidationError({ message: String(e) }),
  },
  {
    retry: {
      times: 3,
      delayMs: 100,
      backoff: "exponential",
      shouldRetry: (e) => e._tag === "NetworkError", // Only retry network errors
    },
  },
);

Async Retry Decisions

For retry decisions that require async operations (rate limits, feature flags, etc.), enrich the error in the catch handler instead of making shouldRetry async:

class ApiError extends TaggedError("ApiError")<{
  message: string;
  rateLimited: boolean;
}>() {}

const result = await Result.tryPromise(
  {
    try: () => callApi(url),
    catch: async (e) => {
      // Fetch async state in catch handler
      const retryAfter = await redis.get(`ratelimit:${userId}`);
      return new ApiError({
        message: (e as Error).message,
        rateLimited: retryAfter !== null,
      });
    },
  },
  {
    retry: {
      times: 3,
      delayMs: 100,
      backoff: "exponential",
      shouldRetry: (e) => !e.rateLimited, // Sync predicate uses enriched error
    },
  },
);

UnhandledException

When Result.try() or Result.tryPromise() catches an exception without a custom handler, the error type is UnhandledException:

import { Result, UnhandledException } from "better-result";

// Automatic — error type is UnhandledException
const result = Result.try(() => JSON.parse(input));
//    ^? Result<unknown, UnhandledException>

// Custom handler — you control the error type
const result = Result.try({
  try: () => JSON.parse(input),
  catch: (e) => new ParseError(e),
});
//    ^? Result<unknown, ParseError>

// Same for async
await Result.tryPromise(() => fetch(url));
//    ^? Promise<Result<Response, UnhandledException>>

Access the original exception via .cause:

if (Result.isError(result)) {
  const original = result.error.cause;
  if (original instanceof SyntaxError) {
    // Handle JSON parse error
  }
}

Panic

Thrown (not returned) when user callbacks throw inside Result operations. Represents a defect in your code, not a domain error.

import { Panic } from "better-result";

// Callback throws → Panic
Result.ok(1).map(() => {
  throw new Error("bug");
}); // throws Panic

// Generator cleanup throws → Panic
Result.gen(function* () {
  try {
    yield* Result.err("expected failure");
  } finally {
    throw new Error("cleanup bug");
  }
}); // throws Panic

// Catch handler throws → Panic
Result.try({
  try: () => riskyOp(),
  catch: () => {
    throw new Error("bug in handler");
  },
}); // throws Panic

Why Panic? Err is for recoverable domain errors. Panic is for bugs — like Rust's panic!(). If your .map() callback throws, that's not an error to handle, it's a defect to fix. Returning Err would collapse type safety (Result<T, E> becomes Result<T, E | unknown>).

Panic properties:

| Property | Type | Description | | --------- | --------- | ----------------------------- | | message | string | Describes where/what panicked | | cause | unknown | The exception that was thrown |

Panic also provides toJSON() for error reporting services (Sentry, etc.).

Tagged Errors

Build exhaustive error handling with discriminated unions:

import { TaggedError, matchError, matchErrorPartial } from "better-result";

// Factory API: TaggedError("Tag")<Props>()
class NotFoundError extends TaggedError("NotFoundError")<{
  id: string;
  message: string;
}>() {}

class ValidationError extends TaggedError("ValidationError")<{
  field: string;
  message: string;
}>() {}

type AppError = NotFoundError | ValidationError;

// Create errors with object args
const err = new NotFoundError({ id: "123", message: "User not found" });

// Exhaustive matching
matchError(error, {
  NotFoundError: (e) => `Missing: ${e.id}`,
  ValidationError: (e) => `Bad field: ${e.field}`,
});

// Partial matching with fallback
matchErrorPartial(
  error,
  { NotFoundError: (e) => `Missing: ${e.id}` },
  (e) => `Unknown: ${e.message}`,
);

// Type guards
TaggedError.is(value); // any tagged error
NotFoundError.is(value); // specific class

For errors with computed messages, add a custom constructor:

class NetworkError extends TaggedError("NetworkError")<{
  url: string;
  status: number;
  message: string;
}>() {
  constructor(args: { url: string; status: number }) {
    super({ ...args, message: `Request to ${args.url} failed: ${args.status}` });
  }
}

new NetworkError({ url: "/api", status: 404 });

Serialization

Convert Results to plain objects for RPC, storage, or server actions:

import { Result, SerializedResult, ResultDeserializationError } from "better-result";

// Serialize to plain object
const result = Result.ok(42);
const serialized = Result.serialize(result);
// { status: "ok", value: 42 }

// Deserialize back to Result instance
const deserialized = Result.deserialize<number, never>(serialized);
// Ok(42) - can use .map(), .andThen(), etc.

// Invalid input returns ResultDeserializationError
const invalid = Result.deserialize({ foo: "bar" });
if (Result.isError(invalid) && ResultDeserializationError.is(invalid.error)) {
  console.log("Bad input:", invalid.error.value);
}

// Typed boundary for Next.js server actions
async function createUser(data: FormData): Promise<SerializedResult<User, ValidationError>> {
  const result = await validateAndCreate(data);
  return Result.serialize(result);
}

// Client-side
const serialized = await createUser(formData);
const result = Result.deserialize<User, ValidationError>(serialized);

API Reference

Result

| Method | Description | | -------------------------------- | --------------------------------------- | | Result.ok(value) | Create success | | Result.err(error) | Create error | | Result.try(fn) | Wrap throwing function | | Result.tryPromise(fn, config?) | Wrap async function with optional retry | | Result.isOk(result) | Type guard for Ok | | Result.isError(result) | Type guard for Err | | Result.gen(fn) | Generator composition | | Result.await(promise) | Wrap Promise for generators | | Result.serialize(result) | Convert Result to plain object | | Result.deserialize(value) | Rehydrate serialized Result (returns Err<ResultDeserializationError> on invalid input) | | Result.partition(results) | Split array into [okValues, errValues] | | Result.flatten(result) | Flatten nested Result |

Instance Methods

| Method | Description | | --------------------- | ------------------------------------- | | .isOk() | Type guard, narrows to Ok | | .isErr() | Type guard, narrows to Err | | .map(fn) | Transform success value | | .mapError(fn) | Transform error value | | .andThen(fn) | Chain Result-returning function | | .andThenAsync(fn) | Chain async Result-returning function | | .match({ ok, err }) | Pattern match | | .unwrap(message?) | Extract value or throw | | .unwrapOr(fallback) | Extract value or return fallback | | .tap(fn) | Side effect on success | | .tapAsync(fn) | Async side effect on success |

TaggedError

| Method | Description | | -------------------------------------- | ---------------------------------- | | TaggedError(tag)<Props>() | Factory for tagged error class | | TaggedError.is(value) | Type guard for any TaggedError | | matchError(err, handlers) | Exhaustive pattern match by _tag | | matchErrorPartial(err, handlers, fb) | Partial match with fallback | | isTaggedError(value) | Type guard (standalone function) | | panic(message, cause?) | Throw unrecoverable Panic | | isPanic(value) | Type guard for Panic |

Type Helpers

| Type | Description | | ------------------------ | ---------------------------- | | InferOk<R> | Extract Ok type from Result | | InferErr<R> | Extract Err type from Result | | SerializedResult<T, E> | Plain object form of Result | | SerializedOk<T> | Plain object form of Ok | | SerializedErr<E> | Plain object form of Err |

Agents & AI

better-result ships with skills for AI coding agents (OpenCode, Claude Code, Codex).

Quick Start

npx better-result init

Interactive setup that:

  1. Installs the better-result package
  2. Optionally fetches source code via opensrc for better AI context
  3. Installs the adoption skill + /adopt-better-result command for your agent
  4. Optionally launches your agent

What the skill does

The /adopt-better-result command guides your AI agent through:

  • Converting try/catch to Result.try/tryPromise
  • Defining TaggedError classes for domain errors
  • Refactoring to generator composition
  • Migrating null checks to Result types

Supported agents

| Agent | Config detected | Skill location | | -------- | ----------------------- | -------------------------------------- | | OpenCode | .opencode/ | .opencode/skill/better-result-adopt/ | | Claude | .claude/, CLAUDE.md | .claude/skills/better-result-adopt/ | | Codex | .codex/, AGENTS.md | .codex/skills/better-result-adopt/ |

Manual usage

If you prefer not to use the interactive CLI:

# Install package
npm install better-result

# Add source for AI context (optional)
npx opensrc better-result

# Then copy skills/ directory to your agent's skill folder

License

MIT