Skip to main content

Overview

better-result ships with skills for AI coding agents (OpenCode, Claude Code, Codex) that automate the adoption of typed error handling in your codebase. These skills guide agents through converting try/catch blocks, defining TaggedError classes, and refactoring to generator composition.
AI agent skills provide contextual guidance and examples, enabling agents to make intelligent decisions about error handling patterns specific to your codebase.

Quick Start

Interactive Setup

The fastest way to get started:
npx better-result init
This interactive CLI:
1

Install package

Adds better-result to your project dependencies
2

Fetch source code (optional)

Downloads better-result source via opensrc for improved AI context
3

Install adoption skill

Installs the /adopt-better-result command for your detected agent
4

Launch agent (optional)

Optionally starts your AI agent with the skill loaded

Supported Agents

better-result provides skills for the following AI coding 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/

What the Adoption Skill Does

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

1. Converting Try/Catch to Result.try

Agent identifies try/catch blocks and transforms them to typed Result returns:
function parseConfig(json: string): Config {
  try {
    return JSON.parse(json);
  } catch (e) {
    throw new ParseError(e);
  }
}

2. Defining TaggedError Classes

Agent creates strongly-typed error classes for domain errors:
class NotFoundError extends TaggedError("NotFoundError")<{
  resource: string;
  id: string;
  message: string;
}>() {
  constructor(args: { resource: string; id: string }) {
    super({
      ...args,
      message: `${args.resource} not found: ${args.id}`,
    });
  }
}

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

type AppError = NotFoundError | ValidationError;

3. Refactoring to Generator Composition

Agent converts callback hell to clean generator syntax:
async function processOrder(orderId: string) {
  try {
    const order = await fetchOrder(orderId);
    if (!order) throw new NotFoundError(orderId);
    const validated = validateOrder(order);
    if (!validated.ok) throw new ValidationError(validated.errors);
    const result = await submitOrder(validated.data);
    return result;
  } catch (e) {
    if (e instanceof NotFoundError) return { error: "not_found" };
    if (e instanceof ValidationError) return { error: "invalid" };
    throw e;
  }
}

4. Migrating Null Checks to Results

Agent converts nullable returns to explicit Result types:
function findUser(id: string): User | null {
  return users.find((u) => u.id === id) ?? null;
}
// Caller must check: if (user === null) ...

AI Agent Migration Strategy

The adoption skill follows a systematic migration strategy:

Start at Boundaries

Agents begin migration at I/O boundaries (API calls, DB queries, file operations) and work inward:
// 1. Start here: API boundary
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
  return Result.tryPromise({
    try: async () => {
      const res = await fetch(`/api/users/${id}`);
      if (!res.ok) throw new ApiError({ status: res.status });
      return res.json();
    },
    catch: (e) => e instanceof ApiError ? e : new UnhandledException({ cause: e }),
  });
}

// 2. Then propagate inward
function processUser(id: string): Result<ProcessedUser, AppError> {
  return Result.gen(async function* () {
    const user = yield* Result.await(fetchUser(id));
    const processed = yield* validateAndProcess(user);
    return Result.ok(processed);
  });
}

Identify Error Categories

Agents categorize errors before migration:
CategoryExampleMigration Target
Domain errorsNotFound, ValidationTaggedError + Result.err
InfrastructureNetwork, DB connectionResult.tryPromise + TaggedError
Bugs/defectsnull deref, type errorLet throw (becomes Panic if in Result callback)

Migration Order

The agent follows this sequence:
1

Define TaggedError classes

Create typed errors for domain error cases
2

Wrap throwing functions

Use Result.try/tryPromise at boundaries
3

Convert error checks

Transform imperative if/else to Result chains
4

Refactor to generators

Use Result.gen for complex flows

Using better-result with LLM-Powered Applications

While the skills are designed for coding agents, better-result is also excellent for applications that integrate with LLMs:

Handling LLM API Errors

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

class RateLimitError extends TaggedError("RateLimitError")<{
  retryAfter: number;
  message: string;
}>() {
  constructor(args: { retryAfterMs: number }) {
    super({
      retryAfter: args.retryAfterMs,
      message: `Rate limited, retry after ${args.retryAfterMs}ms`,
    });
  }
}

class InvalidResponseError extends TaggedError("InvalidResponseError")<{
  reason: string;
  message: string;
}>() {}

class ApiError extends TaggedError("ApiError")<{
  status: number;
  message: string;
}>() {}

type LLMError = RateLimitError | InvalidResponseError | ApiError;

async function callLLM(
  prompt: string
): Promise<Result<string, LLMError>> {
  return Result.tryPromise(
    {
      try: async () => {
        const res = await fetch("https://api.openai.com/v1/chat/completions", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            model: "gpt-4",
            messages: [{ role: "user", content: prompt }],
          }),
        });

        if (res.status === 429) {
          const retryAfter = res.headers.get("retry-after");
          throw new RateLimitError({ 
            retryAfterMs: parseInt(retryAfter || "1000") 
          });
        }

        if (!res.ok) {
          throw new ApiError({ 
            status: res.status, 
            message: `API error: ${res.status}` 
          });
        }

        const data = await res.json();
        const content = data.choices?.[0]?.message?.content;
        
        if (!content) {
          throw new InvalidResponseError({ 
            reason: "No content in response",
            message: "LLM returned empty response"
          });
        }

        return content;
      },
      catch: (e) => {
        if (e instanceof RateLimitError) return e;
        if (e instanceof InvalidResponseError) return e;
        if (e instanceof ApiError) return e;
        return new ApiError({ 
          status: 500, 
          message: String(e) 
        });
      },
    },
    {
      retry: {
        times: 3,
        delayMs: 1000,
        backoff: "exponential",
        shouldRetry: (e) => e._tag === "ApiError" && e.status >= 500,
      },
    }
  );
}

Composing LLM Workflows

Use generator composition for multi-step agentic workflows:
type WorkflowError = LLMError | ValidationError | DatabaseError;

async function agenticWorkflow(
  userInput: string
): Promise<Result<string, WorkflowError>> {
  return Result.gen(async function* () {
    // Step 1: Validate input
    const validated = yield* validateInput(userInput);

    // Step 2: Generate initial response
    const response = yield* Result.await(callLLM(validated.prompt));

    // Step 3: Fact-check with retrieval
    const facts = yield* Result.await(retrieveFacts(response));

    // Step 4: Generate final response with facts
    const final = yield* Result.await(
      callLLM(`${response}\n\nFacts: ${facts}`)
    );

    // Step 5: Store in database
    yield* Result.await(saveToDatabase(final));

    return Result.ok(final);
  });
}

// Handle workflow errors with exhaustive matching
const result = await agenticWorkflow(userInput);

result.match({
  ok: (output) => console.log("Success:", output),
  err: (e) =>
    matchError(e, {
      RateLimitError: (e) => console.log(`Retry after ${e.retryAfter}ms`),
      InvalidResponseError: (e) => console.log(`Bad response: ${e.reason}`),
      ApiError: (e) => console.log(`API error: ${e.status}`),
      ValidationError: (e) => console.log(`Invalid input: ${e.field}`),
      DatabaseError: (e) => console.log(`DB error: ${e.operation}`),
    }),
});

Manual Installation

If you prefer not to use the interactive CLI:
# Install package
npm install better-result

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

# Copy skills manually to your agent's skill folder
# For OpenCode:
cp -r node_modules/better-result/skills/adopt .opencode/skills/better-result-adopt

Source Code Access

The opensrc integration fetches better-result’s full source code to your project:
npx opensrc better-result
This gives AI agents access to:
  • Implementation details (src/result.ts, src/error.ts)
  • Test patterns (src/*.test.ts)
  • Type definitions and internal helpers
  • Complete documentation
Agents use this context to:
  • Understand nuanced behavior
  • Match existing code style
  • Generate more accurate transformations
Source code is placed in opensrc/ and referenced in AGENTS.md for agent discovery.

Common Agent Workflows

Here are typical commands to use with your AI agent:

Adopt better-result in a module

/skill better-result-adopt

Convert the authentication module to use better-result

Migrate from v1 to v2

/skill better-result-migrate-v2

Migrate all TaggedError classes to v2 API

Refactor error handling

Refactor the API client to use Result.tryPromise with retry logic for network errors

Create new TaggedErrors

Create TaggedError classes for the payment processing module: 
PaymentFailedError, InsufficientFundsError, InvalidCardError

Benefits of AI-Assisted Adoption

Using AI agents with better-result skills provides:
  • Consistency: Agents apply patterns uniformly across your codebase
  • Speed: Bulk transformations complete in minutes, not hours
  • Safety: Agents preserve business logic while refactoring error handling
  • Learning: See best practices applied to your specific code
  • Completeness: Agents update callers, imports, and type signatures automatically

Next Steps