better-result-adopt

Adopt better-result in an existing TypeScript codebase. Use when replacing try/catch, Promise rejection handling, null sentinels, or thrown domain exceptions with typed Result workflows.

9 stars

Best use case

better-result-adopt is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Adopt better-result in an existing TypeScript codebase. Use when replacing try/catch, Promise rejection handling, null sentinels, or thrown domain exceptions with typed Result workflows.

Teams using better-result-adopt should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/better-result-adopt/SKILL.md --create-dirs "https://raw.githubusercontent.com/sc30gsw/claude-code-customes/main/sample/harness/tanstack-start/skills/better-result-adopt/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/better-result-adopt/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How better-result-adopt Compares

Feature / Agentbetter-result-adoptStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Adopt better-result in an existing TypeScript codebase. Use when replacing try/catch, Promise rejection handling, null sentinels, or thrown domain exceptions with typed Result workflows.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# better-result Adopt

Adopt `better-result` incrementally in existing codebases without rewriting everything at once.

## When to Use

Use this skill when the user wants to:

- migrate from try/catch to `Result.try` or `Result.tryPromise`
- replace nullable return values with typed `Result<T, E>`
- define domain-specific `TaggedError` types
- refactor nested error handling into `andThen` chains or `Result.gen`
- standardize error handling across a service or module

## Reading Order

| Task                                   | Files to Read                 |
| -------------------------------------- | ----------------------------- |
| Adopt better-result in a module        | This file                     |
| Define or review error types           | `references/tagged-errors.md` |
| Inspect library implementation details | `opensrc/` if present         |

## Prerequisites

Before editing code:

1. Confirm `better-result` is already installed in the target project.
2. Check for an `opensrc/` directory. If present, read the package source there for current patterns.
3. Identify the migration scope first: one file, one module, or one boundary layer.

## Migration Strategy

### 1. Start at boundaries

Begin with I/O boundaries and exception-heavy code:

- HTTP clients
- database access
- file system operations
- parsing and validation
- framework adapters

Do not convert the whole codebase at once.

### 2. Follow official Best Practices

Use the official better-result Best Practices as the source of truth:

- Use `Result` for expected failures that are part of normal flow.
- Wrap throwing third-party and generated-client calls with `Result.try` / `Result.tryPromise`.
- Use `TaggedError` for discriminated domain and infrastructure error unions.
- Preserve context on errors with fields such as `message`, `cause`, ids, status, operation, and reason.
- Compose multi-step flows with `Result.gen`; in async generators, use `yield* Result.await(...)`.
- Avoid premature unwrapping. Keep values in the Result context until a framework or serialization boundary.
- Use `Result.isError` / `Result.isOk` type guards or `.match(...)`; both are valid. Choose the clearer option for the call site.
- Use `matchError` when handling a tagged error union exhaustively.
- Never use `Result<T, any>`.
- Do not ignore Result errors. Handle, propagate, or log them intentionally.
- Do not call `.unwrap()` without a preceding type guard or an intentional, documented throw.
- Test both success and error paths.

### 3. Classify existing failures

| Category              | Examples                    | Target shape                                                   |
| --------------------- | --------------------------- | -------------------------------------------------------------- |
| Domain errors         | not found, validation, auth | `TaggedError` + `Result.err`                                   |
| Infrastructure errors | network, DB, file I/O       | `Result.tryPromise` + mapped error                             |
| Programmer defects    | bad assumptions, null deref | leave throwing; defects become `Panic` inside Result callbacks |

### 4. Migrate in this order

1. Define error types.
2. Wrap throwing boundaries with `Result.try` / `Result.tryPromise`.
3. Replace null or boolean sentinel returns with `Result`.
4. Refactor call sites to propagate `Result` values.
5. Collapse nested branching into `andThen`, `mapError`, or `Result.gen`.

## Serialization Boundaries

When a function is called through RPC, server functions, route loaders, or any serialization boundary:

- Use `Result` internally to model and compose failures.
- Do not return `Ok`, `Err`, `TaggedError`, `Error`, `Response`, or other class instances directly.
- Convert the final outcome to a plain serializable object at the boundary.
- Prefer inferred return types unless the framework produces `unknown`/`any` or the function is an explicit public API boundary.

```ts
const result = await Result.gen(async function* () {
  const user = yield* Result.await(fetchUser(input));
  const session = yield* createSession(user);
  return Result.ok(session);
});

if (Result.isError(result)) {
  return { error: true as const, message: result.error.message };
}

return { error: false as const, session: result.value };
```

## Core Transformations

### Try/catch → `Result.try`

```ts
function parseConfig(json: string): Result<Config, ParseError> {
  return Result.try({
    try: () => JSON.parse(json) as Config,
    catch: (cause) => new ParseError({ cause, message: `Parse failed: ${cause}` }),
  });
}
```

### Async throws → `Result.tryPromise`

```ts
async function fetchUser(id: string): Promise<Result<User, ApiError | UnhandledException>> {
  return Result.tryPromise({
    try: async () => {
      const res = await fetch(`/api/users/${id}`);
      if (!res.ok) throw new ApiError({ status: res.status, message: `API ${res.status}` });
      return res.json() as Promise<User>;
    },
    catch: (cause) => (cause instanceof ApiError ? cause : new UnhandledException({ cause })),
  });
}
```

### Null sentinel → `Result`

```ts
function findUser(id: string): Result<User, NotFoundError> {
  const user = users.find((candidate) => candidate.id === id);
  return user
    ? Result.ok(user)
    : Result.err(new NotFoundError({ id, message: `User ${id} not found` }));
}
```

### Nested flow → `Result.gen`

```ts
async function processOrder(orderId: string): Promise<Result<OrderResult, OrderError>> {
  return Result.gen(async function* () {
    const order = yield* Result.await(fetchOrder(orderId));
    const validated = yield* validateOrder(order);
    const result = yield* Result.await(submitOrder(validated));
    return Result.ok(result);
  });
}
```

## Execution Workflow

1. Audit the target module for `try`, `catch`, `.catch(...)`, `throw`, `null`, `undefined`, and status-flag error handling.
2. Define or update `TaggedError` classes before changing control flow.
3. Convert boundary functions first and change their signatures to `Result<T, E>` or `Promise<Result<T, E>>`.
4. Update immediate callers so they handle or propagate the new `Result`.
5. Where multiple Result-returning steps compose, use `Result.gen` or `andThen`.
6. Preserve error context by keeping `cause`, IDs, messages, and other structured fields.
7. Run tests and add coverage for both success and error paths.

## Completion Criteria

A migration is complete when:

- target functions no longer rely on try/catch for expected domain failures
- nullable or sentinel error returns are replaced with explicit `Result` values
- domain failures use typed `TaggedError` classes
- callers either propagate `Result` or explicitly unwrap/match it
- serialization boundaries return plain objects, not Result or Error instances
- tests cover at least one success path and one representative error path

## Common Pitfalls

- Over-wrapping everything instead of starting at boundaries
- Losing original failure context when mapping errors
- Mixing `throw`-based and `Result`-based APIs deep in the same flow
- Catching `Panic` instead of fixing the underlying defect
- Returning Result or TaggedError instances from server functions or RPC handlers that require serialization
- Treating `.match(...)` as mandatory when a `Result.isError` / `Result.isOk` type guard is clearer

## In This Reference

| File                          | Purpose                                                   |
| ----------------------------- | --------------------------------------------------------- |
| `references/tagged-errors.md` | TaggedError patterns, matching, type guards, and examples |

If `opensrc/` exists, treat it as the source of truth for implementation details and current API behavior.

Related Skills

test

9
from sc30gsw/claude-code-customes

Advanced test implementation command with unit/E2E support, auto-execution, and smart fixing capabilities

serena

9
from sc30gsw/claude-code-customes

Token-efficient Serena MCP command for structured app development and problem-solving

project-guidelines-example

9
from sc30gsw/claude-code-customes

Example project-specific skill template based on a real production application.

notion-bug-pr

9
from sc30gsw/claude-code-customes

Skill that pulls bug tickets (titles containing 「不具合」) from a Notion database, investigates root cause in a GitHub repo, applies fixes, and opens draft PRs. Supports three modes — daily recurring schedule, one-shot at a specific time, or immediate on-demand run. Takes three or four args: Notion database URL, repo path or name, and either HH:MM (daily), "once HH:MM" (one-shot), or "now" (immediate).

graphify

9
from sc30gsw/claude-code-customes

any input (code, docs, papers, images, videos) to knowledge graph. Use when user asks any question about a codebase, documents, or project content - especially if graphify-out/ exists, treat the question as a /graphify query.

chrome

9
from sc30gsw/claude-code-customes

Comprehensive Chrome DevTools development system with native Chrome capabilities for debugging, E2E testing, performance analysis, and browser automation

webapp-testing

9
from sc30gsw/claude-code-customes

Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.

web-design-guidelines

9
from sc30gsw/claude-code-customes

Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".

typescript-advanced-types

9
from sc30gsw/claude-code-customes

Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.

tanstack-start

9
from sc30gsw/claude-code-customes

Full-stack React framework powered by TanStack Router with SSR, streaming, server functions, and deployment to any hosting provider.

tanstack-start-server-fn-testing

9
from sc30gsw/claude-code-customes

Unit-test TanStack Start createServerFn handlers via a global vi.mock that combines two patterns from Discussion #2701

tailwind-css-patterns

9
from sc30gsw/claude-code-customes

Provides comprehensive Tailwind CSS utility-first styling patterns including responsive design, layout utilities, flexbox, grid, spacing, typography, colors, and modern CSS best practices. Use when styling React/Vue/Svelte components, building responsive layouts, implementing design systems, or optimizing CSS workflow.