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

- Skill: `sc30gsw/better-result-adopt` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add sc30gsw/better-result-adopt`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sc30gsw/better-result-adopt/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: sc30gsw (https://skillmd.com/u/sc30gsw)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/sc30gsw/better-result-adopt

---


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

