better-result

repository·main·Indexed 23 days ago

https://github.com/dmmulroy/better-result

A lightweight Result type for TypeScript that enables functional error handling and generator-based composition to avoid nested callbacks and early returns. Version 2.10.0 features include Result.gen for chaining operations, TaggedError for exhaustive pattern matching, and utilities for serializing results across boundaries. It provides methods for wrapping throwing functions via Result.try and Result.tryPromise, as well as recovery and observation tools like tryRecover and tap.

Tokens
15.9K
Snippets
40
Records
66
Agent score
80%

What's inside better-result

  1. Compose multiple Results with Generators

    main

    Use Result.gen to chain multiple Result operations using a generator function. This avoids nested callbacks and early returns. Use yield* to unwrap a Result or short-circuit on error. For asynchronous operations, use Result.gen with an async function* and yield* Result.await(promise).

    // Synchronous composition
    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);
    });
    
    // Async composition
    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 });
    });
  2. Use the new Panic mechanism for unrecoverable errors

    main

    v2 introduces Panic to handle unrecoverable errors (bugs) that occur inside Result operations, such as when a user-provided callback throws or a generator cleanup fails. This distinguishes between recoverable domain errors (Err) and developer bugs (Panic).

    • Automatic Panics: Occur when a callback inside .map() or similar operations throws, or when a Result.gen cleanup block throws.
    • Manual Panics: Use the panic(message, cause) function.
    • Type Guarding: Use isPanic(error) to identify a panic.
    import { Panic, panic, isPanic } from "better-result";
    
    // Manual panic
    panic("something went wrong", cause);
    
    // Type guard
    if (isPanic(error)) {
      console.log(error.message, error.cause);
    }
    import { Panic, panic, isPanic } from "better-result";
    
    // Callbacks that throw now cause Panic instead of corrupting state
    Result.ok(1).map(() => {
      throw new Error("bug");
    }); // throws Panic
    
    // Generator cleanup throws → Panic
    Result.gen(function* () {
      try {
        yield* Result.err("expected");
      } finally {
        throw new Error("cleanup bug"); // throws Panic
      }
    });
    
    // Manual panic
    panic("something went wrong", cause);
    
    // Type guard
    if (isPanic(error)) {
      console.log(error.message, error.cause);
    }
  3. Yield TaggedErrors in Result.gen

    main

    In a Result.gen generator, you can yield* a TaggedError instance to short-circuit the generator and return an Err containing that error. This is equivalent to yield* Result.err(error) but more concise. It does not throw; it returns a Result object.

    Yielding errors also allows them to compose with other Result values, contributing to the inferred error union type of the generator.

    const result = Result.gen(function* () {
      // Short-circuits with NotFoundError
      yield* new NotFoundError({ id: "123", message: "missing" });
      
      return Result.ok("never reached");
    });
    // Result<string, NotFoundError>
  4. Understand and catch Panics

    main

    Panic is thrown (not returned) when a user-provided callback (like in .map(), Result.gen, or Result.try) throws an exception. This represents a code defect rather than a recoverable domain error.

    To catch a Panic for error reporting, use isPanic(error), Panic.is(error), or error instanceof Panic.

    Panic Properties:

    • message: string describing the panic.
    • cause: unknown, the original exception.

    Why Panic? Using Err for bugs would compromise type safety by forcing Result<T, E | unknown>. Panic preserves the integrity of the Result type.

    import { Panic, isPanic } from "better-result";
    
    // Callback throws → Panic
    Result.ok(1).map(() => {
      throw new Error("bug");
    }); // throws Panic
    
    // Catching Panic (for error reporting)
    try {
      result.map(() => {
        throw new Error("bug");
      });
    } catch (error) {
      if (isPanic(error)) {
        console.error("Defect:", error.message, error.cause);
      }
    
      if (Panic.is(error)) {
        // same behavior
      }
    
      if (error instanceof Panic) {
        // works too
      }
    }
  5. Migrate TaggedError class definitions from v1 to v2

    main

    In better-result v2, TaggedError is used as a factory function to define error classes. Instead of manually defining a _tag property and calling super(message), you pass the tag name to TaggedError() and provide a generic type for the error's properties (which must include message).

    Pattern 1: Simple class

    For simple errors, pass the tag and the property types to the factory, then extend the resulting class.

    Pattern 2: Computed messages

    If the message depends on other properties, implement a constructor that accepts an arguments object and calls super() with the computed message included in the object.

    Pattern 3: Validation logic

    Validation logic should be placed inside the constructor before calling super().

    Pattern 4: Extra runtime properties

    Properties not passed in the constructor (like timestamps) should be included in the generic type definition and added via the super() call.

  6. Migrate TaggedError from v1 to v2

    main

    When upgrading better-result from v1 to v2, you must migrate error definitions from the class-based TaggedError API to the new factory-based API.

    Key Changes

    • Class Declaration: Instead of class FooError extends TaggedError, use the factory pattern: class FooError extends TaggedError("FooError")<Props>() {}.
    • Tag Generation: You no longer need to manually define readonly _tag = "FooError" as const; the factory generates this for you.
    • Constructor Arguments: Positional arguments are replaced by a single object argument. For example, new FooError("123") becomes new FooError({ id: "123", message: "..." }).
    • Static Helpers: Static methods on TaggedError have been replaced by standalone helper functions:
      • TaggedError.match(...) $\rightarrow$ matchError(...)
      • TaggedError.matchPartial(...) $\rightarrow$ matchErrorPartial(...)
      • TaggedError.isTaggedError(...) $\rightarrow$ isTaggedError(...) or TaggedError.is(...)
  7. Migrate TaggedError class definitions to v2

    main

    In v2, TaggedError uses a factory pattern that eliminates the need for manual _tag declarations. You must pass the tag name as an argument to the TaggedError factory and provide a type definition for the error properties.

    v1 Pattern:

    class NotFoundError extends TaggedError {
      readonly _tag = "NotFoundError" as const;
      constructor(readonly id: string) {
        super(`Not found: ${id}`);
      }
    }

    v2 Pattern:

    class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string; }> {}
    
    // Note: Constructor calls must now pass an object containing all properties
    const err = new NotFoundError({ id: "123", message: "Not found: 123" });
    class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string; }> {}
    
    const err = new NotFoundError({ id: "123", message: "Not found: 123" });
  8. Quick Start with better-result

    main

    Use Result.try to wrap functions that might throw, and Result.isOk to check the outcome. You can also use .match() for pattern matching on the result.

    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}`,
    });
  9. Migration Workflow for TaggedError

    main

    Follow these steps to ensure a complete migration from better-result v1 to v2:

    1. Identify Targets: Search the codebase for extends TaggedError and readonly _tag =.
    2. Extract Metadata: For each class, identify the _tag literal, constructor parameters, runtime properties, and any validation or computed message logic.
    3. Rewrite Class: Use the TaggedError("Tag")<Props>() factory pattern.
    4. Handle Custom Logic: If the error requires custom logic (like validation or computing a message), implement a custom constructor that accepts an object and calls super({...}).
    5. Update Call Sites: Change all new ErrorClass(arg1, arg2) calls to the new object-based shape new ErrorClass({ arg1, arg2, message }).
    6. Replace Helpers: Swap TaggedError.match, TaggedError.matchPartial, and TaggedError.isTaggedError with their standalone v2 counterparts.
    7. Update Imports: Ensure all new standalone helpers are correctly imported.
    8. Verify: Run tests and perform a final search for old API patterns.
  10. Install better-result skills for AI Agents

    main

    The project provides portable SKILL.md skills to help AI agents adopt or migrate to better-result.

    Available Skills

    • better-result-adopt: Guides an agent through converting try/catch to Result methods, defining TaggedError classes, and refactoring to Result.gen.
    • better-result-migrate-v2: Guides an agent through migrating TaggedError from v1 to the v2 factory syntax.

    Installation via skills.sh

    Use npx skills to add these to your agent's environment:

    npx skills add dmmulroy/better-result@better-result-adopt
    npx skills add dmmulroy/better-result@better-result-migrate-v2

    To install globally without prompts:

    npx skills add dmmulroy/better-result@better-result-adopt -g -y

    Manual Installation

    If your agent does not support skills.sh, manually copy the following directories into the agent's skills folder:

    • skills/better-result-adopt/
    • skills/better-result-migrate-v2/
    npx skills add dmmulroy/better-result@better-result-adopt
    npx skills add dmmulroy/better-result@better-result-migrate-v2
  11. Manually install better-result skills

    main
    If you are not using skills.sh tooling, you can manually install skills by copying the skill directories directly into your agent's configured skills folder. Each skill is self-contained and utilizes standard SKILL.md frontmatter and optional references/ files.
  12. Install better-result skills via skills.sh

    main

    You can add specific better-result skills to your SKILL.md-compatible agent using the npx skills add command.

    Available skills:

    • better-result-adopt: Use this to adopt better-result into an existing codebase.
    • better-result-migrate-v2: Use this to migrate from v1 TaggedError usage to the v2 API.

    To install globally without interactive prompts, use the -g -y flags.