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:
- Confirm
better-result is already installed in the target project.
- Check for an
opensrc/ directory. If present, read the package source there for current patterns.
- 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
- Define error types.
- Wrap throwing boundaries with
Result.try / Result.tryPromise.
- Replace null or boolean sentinel returns with
Result.
- Refactor call sites to propagate
Result values.
- 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.
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
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
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
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
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
- Audit the target module for
try, catch, .catch(...), throw, null, undefined, and status-flag error handling.
- Define or update
TaggedError classes before changing control flow.
- Convert boundary functions first and change their signatures to
Result<T, E> or Promise<Result<T, E>>.
- Update immediate callers so they handle or propagate the new
Result.
- Where multiple Result-returning steps compose, use
Result.gen or andThen.
- Preserve error context by keeping
cause, IDs, messages, and other structured fields.
- 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.
1---2name: better-result-adopt3description: 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.4---56# better-result Adopt78Adopt `better-result` incrementally in existing codebases without rewriting everything at once.910## When to Use1112Use this skill when the user wants to:1314- migrate from try/catch to `Result.try` or `Result.tryPromise`15- replace nullable return values with typed `Result<T, E>`16- define domain-specific `TaggedError` types17- refactor nested error handling into `andThen` chains or `Result.gen`18- standardize error handling across a service or module1920## Reading Order2122| Task | Files to Read |23| -------------------------------------- | ----------------------------- |24| Adopt better-result in a module | This file |25| Define or review error types | `references/tagged-errors.md` |26| Inspect library implementation details | `opensrc/` if present |2728## Prerequisites2930Before editing code:31321. Confirm `better-result` is already installed in the target project.332. Check for an `opensrc/` directory. If present, read the package source there for current patterns.343. Identify the migration scope first: one file, one module, or one boundary layer.3536## Migration Strategy3738### 1. Start at boundaries3940Begin with I/O boundaries and exception-heavy code:4142- HTTP clients43- database access44- file system operations45- parsing and validation46- framework adapters4748Do not convert the whole codebase at once.4950### 2. Follow official Best Practices5152Use the official better-result Best Practices as the source of truth:5354- Use `Result` for expected failures that are part of normal flow.55- Wrap throwing third-party and generated-client calls with `Result.try` / `Result.tryPromise`.56- Use `TaggedError` for discriminated domain and infrastructure error unions.57- Preserve context on errors with fields such as `message`, `cause`, ids, status, operation, and reason.58- Compose multi-step flows with `Result.gen`; in async generators, use `yield* Result.await(...)`.59- Avoid premature unwrapping. Keep values in the Result context until a framework or serialization boundary.60- Use `Result.isError` / `Result.isOk` type guards or `.match(...)`; both are valid. Choose the clearer option for the call site.61- Use `matchError` when handling a tagged error union exhaustively.62- Never use `Result<T, any>`.63- Do not ignore Result errors. Handle, propagate, or log them intentionally.64- Do not call `.unwrap()` without a preceding type guard or an intentional, documented throw.65- Test both success and error paths.6667### 3. Classify existing failures6869| Category | Examples | Target shape |70| --------------------- | --------------------------- | -------------------------------------------------------------- |71| Domain errors | not found, validation, auth | `TaggedError` + `Result.err` |72| Infrastructure errors | network, DB, file I/O | `Result.tryPromise` + mapped error |73| Programmer defects | bad assumptions, null deref | leave throwing; defects become `Panic` inside Result callbacks |7475### 4. Migrate in this order76771. Define error types.782. Wrap throwing boundaries with `Result.try` / `Result.tryPromise`.793. Replace null or boolean sentinel returns with `Result`.804. Refactor call sites to propagate `Result` values.815. Collapse nested branching into `andThen`, `mapError`, or `Result.gen`.8283## Serialization Boundaries8485When a function is called through RPC, server functions, route loaders, or any serialization boundary:8687- Use `Result` internally to model and compose failures.88- Do not return `Ok`, `Err`, `TaggedError`, `Error`, `Response`, or other class instances directly.89- Convert the final outcome to a plain serializable object at the boundary.90- Prefer inferred return types unless the framework produces `unknown`/`any` or the function is an explicit public API boundary.9192```ts93const result = await Result.gen(async function* () {94 const user = yield* Result.await(fetchUser(input));95 const session = yield* createSession(user);96 return Result.ok(session);97});9899if (Result.isError(result)) {100 return { error: true as const, message: result.error.message };101}102103return { error: false as const, session: result.value };104```105106## Core Transformations107108### Try/catch → `Result.try`109110```ts111function parseConfig(json: string): Result<Config, ParseError> {112 return Result.try({113 try: () => JSON.parse(json) as Config,114 catch: (cause) => new ParseError({ cause, message: `Parse failed: ${cause}` }),115 });116}117```118119### Async throws → `Result.tryPromise`120121```ts122async function fetchUser(id: string): Promise<Result<User, ApiError | UnhandledException>> {123 return Result.tryPromise({124 try: async () => {125 const res = await fetch(`/api/users/${id}`);126 if (!res.ok) throw new ApiError({ status: res.status, message: `API ${res.status}` });127 return res.json() as Promise<User>;128 },129 catch: (cause) => (cause instanceof ApiError ? cause : new UnhandledException({ cause })),130 });131}132```133134### Null sentinel → `Result`135136```ts137function findUser(id: string): Result<User, NotFoundError> {138 const user = users.find((candidate) => candidate.id === id);139 return user140 ? Result.ok(user)141 : Result.err(new NotFoundError({ id, message: `User ${id} not found` }));142}143```144145### Nested flow → `Result.gen`146147```ts148async function processOrder(orderId: string): Promise<Result<OrderResult, OrderError>> {149 return Result.gen(async function* () {150 const order = yield* Result.await(fetchOrder(orderId));151 const validated = yield* validateOrder(order);152 const result = yield* Result.await(submitOrder(validated));153 return Result.ok(result);154 });155}156```157158## Execution Workflow1591601. Audit the target module for `try`, `catch`, `.catch(...)`, `throw`, `null`, `undefined`, and status-flag error handling.1612. Define or update `TaggedError` classes before changing control flow.1623. Convert boundary functions first and change their signatures to `Result<T, E>` or `Promise<Result<T, E>>`.1634. Update immediate callers so they handle or propagate the new `Result`.1645. Where multiple Result-returning steps compose, use `Result.gen` or `andThen`.1656. Preserve error context by keeping `cause`, IDs, messages, and other structured fields.1667. Run tests and add coverage for both success and error paths.167168## Completion Criteria169170A migration is complete when:171172- target functions no longer rely on try/catch for expected domain failures173- nullable or sentinel error returns are replaced with explicit `Result` values174- domain failures use typed `TaggedError` classes175- callers either propagate `Result` or explicitly unwrap/match it176- serialization boundaries return plain objects, not Result or Error instances177- tests cover at least one success path and one representative error path178179## Common Pitfalls180181- Over-wrapping everything instead of starting at boundaries182- Losing original failure context when mapping errors183- Mixing `throw`-based and `Result`-based APIs deep in the same flow184- Catching `Panic` instead of fixing the underlying defect185- Returning Result or TaggedError instances from server functions or RPC handlers that require serialization186- Treating `.match(...)` as mandatory when a `Result.isError` / `Result.isOk` type guard is clearer187188## In This Reference189190| File | Purpose |191| ----------------------------- | --------------------------------------------------------- |192| `references/tagged-errors.md` | TaggedError patterns, matching, type guards, and examples |193194If `opensrc/` exists, treat it as the source of truth for implementation details and current API behavior.