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.tryorResult.tryPromise - replace nullable return values with typed
Result<T, E> - define domain-specific
TaggedErrortypes - refactor nested error handling into
andThenchains orResult.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-resultis 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. 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 |
3. 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
Resultvalues. - Collapse nested branching into
andThen,mapError, orResult.gen.
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
TaggedErrorclasses before changing control flow. - Convert boundary functions first and change their signatures to
Result<T, E>orPromise<Result<T, E>>. - Update immediate callers so they handle or propagate the new
Result. - Where multiple Result-returning steps compose, use
Result.genorandThen. - 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
Resultvalues - domain failures use typed
TaggedErrorclasses - callers either propagate
Resultor explicitly unwrap/match it - 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 andResult-based APIs deep in the same flow - Catching
Panicinstead of fixing the underlying defect
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.