You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

better-result

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

better-result

Lightweight Result type with generator-based composition

latest
Source
npmnpm
Version
2.7.0
Version published
Weekly downloads
17K
-6.06%
Maintainers
1
Weekly downloads
 
Created
Source

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
  • Transforming Results
  • Handling Errors
  • Extracting Values
  • Generator Composition
  • Retry Support
  • UnhandledException
  • Panic
  • Tagged Errors
  • Serialization
  • API Reference
  • Agents & AI

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:

PropertyTypeDescription
messagestringDescribes where/what panicked
causeunknownThe 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

MethodDescription
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

MethodDescription
.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

MethodDescription
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

TypeDescription
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:

  • Installs the better-result package
  • Optionally fetches source code via opensrc for better AI context
  • Installs the adoption skill + /adopt-better-result command for your agent
  • 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

AgentConfig detectedSkill 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

Keywords

error-handling

FAQs

Package last updated on 31 Jan 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts