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.
Adopt better-result incrementally in existing codebases without rewriting everything at once.
Use this skill when the user wants to:
Result.try or Result.tryPromiseResult<T, E>TaggedError typesandThen chains or Result.gen| 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 |
Before editing code:
better-result is already installed in the target project.opensrc/ directory. If present, read the package source there for current patterns.Begin with I/O boundaries and exception-heavy code:
Do not convert the whole codebase at once.
| 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 |
Result.try / Result.tryPromise.Result.Result values.andThen, mapError, or Result.gen.Result.tryfunction parseConfig(json: string): Result<Config, ParseError> {
return Result.try({
try: () => JSON.parse(json) as Config,
catch: (cause) => new ParseError({ cause, message: `Parse failed: ${cause}` }),
});
}
Result.tryPromiseasync 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 })),
});
}
Resultfunction 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` }));
}
Result.genasync 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);
});
}
try, catch, .catch(...), throw, null, undefined, and status-flag error handling.TaggedError classes before changing control flow.Result<T, E> or Promise<Result<T, E>>.Result.Result.gen or andThen.cause, IDs, messages, and other structured fields.A migration is complete when:
Result valuesTaggedError classesResult or explicitly unwrap/match itthrow-based and Result-based APIs deep in the same flowPanic instead of fixing the underlying defect| 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.