| description | 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. |
| metadata | {"github-path":"skills/better-result-adopt","github-ref":"refs/tags/v2.8.2","github-repo":"https://github.com/dmmulroy/better-result","github-tree-sha":"44f00397d20b3b41137a17514b252f874e7d6bc3"} |
| name | better-result-adopt |
| references | ["references/tagged-errors.md"] |
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. 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
Result values.
- Collapse nested branching into
andThen, mapError, or Result.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
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
- 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
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.