
Better Result Adopt
- 33 installs
- 1.2k repo stars
- Updated April 12, 2026
- davis7dotsh/better-context
Migrate a codebase from try/catch or Promise-based error handling to typed better-result Result types.
About
Guides migrating existing try/catch and Promise-based error handling to typed Result types with better-result. A developer uses it when adopting railway-oriented error handling in a codebase.
- Start migration at I/O boundaries and work inward
- Categorize errors into TaggedError and Result targets
Better Result Adopt by the numbers
- 33 all-time installs (skills.sh)
- Ranked #3,349 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davis7dotsh/better-context --skill better-result-adoptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 1.2k |
| Last updated | April 12, 2026 |
| Repository | davis7dotsh/better-context ↗ |
What it does
Migrate a codebase from try/catch or Promise-based error handling to typed better-result Result types.
Files
better-result Adoption
Migrate existing error handling (try/catch, Promise rejections, thrown exceptions) to typed Result-based error handling with better-result.
When to Use
- Adopting better-result in existing codebase
- Converting try/catch blocks to Result types
- Replacing thrown exceptions with typed errors
- Migrating Promise-based code to Result.tryPromise
- Introducing railway-oriented programming patterns
Migration Strategy
1. Start at Boundaries
Begin migration at I/O boundaries (API calls, DB queries, file ops) and work inward. Don't attempt full-codebase migration at once.
2. Identify Error Categories
Before migrating, categorize errors in target code:
| Category | Example | Migration Target |
|---|---|---|
| Domain errors | NotFound, Validation | TaggedError + Result.err |
| Infrastructure | Network, DB connection | Result.tryPromise + TaggedError |
| Bugs/defects | null deref, type error | Let throw (becomes Panic if in Result callback) |
3. Migration Order
1. Define TaggedError classes for domain errors 2. Wrap throwing functions with Result.try/tryPromise 3. Convert imperative error checks to Result chains 4. Refactor callbacks to generator composition
Pattern Transformations
Try/Catch to Result.try
// BEFORE
function parseConfig(json: string): Config {
try {
return JSON.parse(json);
} catch (e) {
throw new ParseError(e);
}
}
// AFTER
function parseConfig(json: string): Result<Config, ParseError> {
return Result.try({
try: () => JSON.parse(json) as Config,
catch: (e) => new ParseError({ cause: e, message: `Parse failed: ${e}` }),
});
}Async/Await to Result.tryPromise
// BEFORE
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new ApiError(res.status);
return res.json();
}
// AFTER
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: (e) => (e instanceof ApiError ? e : new UnhandledException({ cause: e })),
});
}Null Checks to Result
// BEFORE
function findUser(id: string): User | null {
return users.find((u) => u.id === id) ?? null;
}
// Caller must check: if (user === null) ...
// AFTER
function findUser(id: string): Result<User, NotFoundError> {
const user = users.find((u) => u.id === id);
return user
? Result.ok(user)
: Result.err(new NotFoundError({ id, message: `User ${id} not found` }));
}
// Caller: yield* findUser(id) in Result.gen, or .match()Callback Hell to Generator
// BEFORE
async function processOrder(orderId: string) {
try {
const order = await fetchOrder(orderId);
if (!order) throw new NotFoundError(orderId);
const validated = validateOrder(order);
if (!validated.ok) throw new ValidationError(validated.errors);
const result = await submitOrder(validated.data);
return result;
} catch (e) {
if (e instanceof NotFoundError) return { error: "not_found" };
if (e instanceof ValidationError) return { error: "invalid" };
throw e;
}
}
// AFTER
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);
});
}
// Error type is union of all yielded errorsDefining TaggedErrors
See references/tagged-errors.md for TaggedError patterns.
Workflow
1. Check for source reference: Look for opensrc/ directory - if present, read the better-result source code for implementation details and patterns 2. Audit: Find try/catch, Promise.catch, thrown errors in target module 3. Define errors: Create TaggedError classes for domain errors 4. Wrap boundaries: Use Result.try/tryPromise at I/O points 5. Chain operations: Convert if/else error checks to .andThen or Result.gen 6. Update signatures: Change return types to Result<T, E> 7. Update callers: Propagate Result handling up call stack 8. Test: Verify error paths with .match or type narrowing
Common Pitfalls
- Over-wrapping: Don't wrap every function. Start at boundaries, propagate inward.
- Losing error info: Always include cause/context in TaggedError constructors.
- Mixing paradigms: Once a module returns Result, callers should too (or explicitly .unwrap).
- Ignoring Panic: Callbacks that throw become Panic. Fix the bug, don't catch Panic.
References
- TaggedError Patterns - Defining and matching typed errors
opensrc/directory (if present) - Full better-result source code for deeper context
TaggedError Patterns
Defining Errors
Simple Error (no computed message)
import { TaggedError } from "better-result";
class NotFoundError extends TaggedError("NotFoundError")<{
resource: string;
id: string;
message: string;
}>() {}
// Usage
new NotFoundError({ resource: "User", id: "123", message: "User 123 not found" });Error with Computed Message
Keep constructor for derived message:
class NotFoundError extends TaggedError("NotFoundError")<{
resource: string;
id: string;
message: string;
}>() {
constructor(args: { resource: string; id: string }) {
super({ ...args, message: `${args.resource} not found: ${args.id}` });
}
}
// Usage: new NotFoundError({ resource: "User", id: "123" })Error with Cause
Wrap underlying exceptions:
class DatabaseError extends TaggedError("DatabaseError")<{
operation: string;
message: string;
cause: unknown;
}>() {
constructor(args: { operation: string; cause: unknown }) {
const msg = args.cause instanceof Error ? args.cause.message : String(args.cause);
super({ ...args, message: `DB ${args.operation} failed: ${msg}` });
}
}
// Usage in Result.tryPromise
Result.tryPromise({
try: () => db.query(sql),
catch: (e) => new DatabaseError({ operation: "query", cause: e }),
});Error with Validation/Runtime Props
class RateLimitError extends TaggedError("RateLimitError")<{
retryAfter: number;
message: string;
}>() {
constructor(args: { retryAfterMs: number }) {
super({
retryAfter: args.retryAfterMs,
message: `Rate limited, retry after ${args.retryAfterMs}ms`,
});
}
}Error Unions
Group related errors for function signatures:
// Domain errors
class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string }>() {}
class ValidationError extends TaggedError("ValidationError")<{ field: string; message: string }>() {}
class AuthError extends TaggedError("AuthError")<{ reason: string; message: string }>() {}
// Union type
type AppError = NotFoundError | ValidationError | AuthError;
// Function signature
function processRequest(req: Request): Result<Response, AppError> { ... }Matching Errors
Exhaustive Match
Compiler ensures all error types handled:
import { matchError } from "better-result";
const message = matchError(error, {
NotFoundError: (e) => `Missing: ${e.id}`,
ValidationError: (e) => `Invalid: ${e.field}`,
AuthError: (e) => `Unauthorized: ${e.reason}`,
});Partial Match with Fallback
Handle subset, catch-all for rest:
import { matchErrorPartial } from "better-result";
const message = matchErrorPartial(
error,
{ NotFoundError: (e) => `Missing: ${e.id}` },
(e) => `Error: ${e.message}`, // fallback for ValidationError, AuthError
);Type Guards
import { isTaggedError, TaggedError } from "better-result";
// Check any tagged error
if (isTaggedError(value)) {
console.log(value._tag);
}
// Check specific error class
if (NotFoundError.is(value)) {
console.log(value.id); // narrowed to NotFoundError
}
// Also available
TaggedError.is(value); // same as isTaggedErrorIn Result.match
result.match({
ok: (value) => handleSuccess(value),
err: (e) =>
matchError(e, {
NotFoundError: (e) => handleNotFound(e),
ValidationError: (e) => handleValidation(e),
}),
});Pipeable Style
matchError/matchErrorPartial support data-last for pipelines:
const handler = matchError({
NotFoundError: (e) => `Missing: ${e.id}`,
ValidationError: (e) => `Invalid: ${e.field}`,
});
pipe(error, handler);Converting Existing Errors
// FROM: class hierarchy
class NotFoundError extends AppError {
constructor(public id: string) {
super(`Not found: ${id}`);
}
}
// TO: TaggedError
class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string }>() {
constructor(args: { id: string }) {
super({ ...args, message: `Not found: ${args.id}` });
}
}
// FROM: string/generic errors
throw "User not found";
// TO: typed Result
return Result.err(new NotFoundError({ id, message: "User not found" }));