Overview
better-result ships with skills for AI coding agents (OpenCode, Claude Code, Codex) that automate the adoption of typed error handling in your codebase. These skills guide agents through converting try/catch blocks, defining TaggedError classes, and refactoring to generator composition.
AI agent skills provide contextual guidance and examples, enabling agents to make intelligent decisions about error handling patterns specific to your codebase.
Quick Start
Interactive Setup
The fastest way to get started:
This interactive CLI:
Install package
Adds better-result to your project dependencies
Fetch source code (optional)
Downloads better-result source via opensrc for improved AI context Install adoption skill
Installs the /adopt-better-result command for your detected agent
Launch agent (optional)
Optionally starts your AI agent with the skill loaded
Supported Agents
better-result provides skills for the following AI coding agents:
| Agent | Config Detected | Skill Location |
|---|
| OpenCode | .opencode/ | .opencode/skill/better-result-adopt/ |
| Claude | .claude/, CLAUDE.md | .claude/skills/better-result-adopt/ |
| Codex | .codex/, AGENTS.md | .codex/skills/better-result-adopt/ |
What the Adoption Skill Does
The /adopt-better-result command guides your AI agent through:
1. Converting Try/Catch to Result.try
Agent identifies try/catch blocks and transforms them to typed Result returns:
function parseConfig(json: string): Config {
try {
return JSON.parse(json);
} catch (e) {
throw new ParseError(e);
}
}
2. Defining TaggedError Classes
Agent creates strongly-typed error classes for domain errors:
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}`,
});
}
}
class ValidationError extends TaggedError("ValidationError")<{
field: string;
message: string;
}>() {}
type AppError = NotFoundError | ValidationError;
3. Refactoring to Generator Composition
Agent converts callback hell to clean generator syntax:
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;
}
}
4. Migrating Null Checks to Results
Agent converts nullable returns to explicit Result types:
function findUser(id: string): User | null {
return users.find((u) => u.id === id) ?? null;
}
// Caller must check: if (user === null) ...
AI Agent Migration Strategy
The adoption skill follows a systematic migration strategy:
Start at Boundaries
Agents begin migration at I/O boundaries (API calls, DB queries, file operations) and work inward:
// 1. Start here: API boundary
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
return Result.tryPromise({
try: async () => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new ApiError({ status: res.status });
return res.json();
},
catch: (e) => e instanceof ApiError ? e : new UnhandledException({ cause: e }),
});
}
// 2. Then propagate inward
function processUser(id: string): Result<ProcessedUser, AppError> {
return Result.gen(async function* () {
const user = yield* Result.await(fetchUser(id));
const processed = yield* validateAndProcess(user);
return Result.ok(processed);
});
}
Identify Error Categories
Agents categorize errors before migration:
| 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) |
Migration Order
The agent follows this sequence:
Define TaggedError classes
Create typed errors for domain error cases
Wrap throwing functions
Use Result.try/tryPromise at boundaries
Convert error checks
Transform imperative if/else to Result chains
Refactor to generators
Use Result.gen for complex flows
Using better-result with LLM-Powered Applications
While the skills are designed for coding agents, better-result is also excellent for applications that integrate with LLMs:
Handling LLM API Errors
import { Result, TaggedError } from "better-result";
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`,
});
}
}
class InvalidResponseError extends TaggedError("InvalidResponseError")<{
reason: string;
message: string;
}>() {}
class ApiError extends TaggedError("ApiError")<{
status: number;
message: string;
}>() {}
type LLMError = RateLimitError | InvalidResponseError | ApiError;
async function callLLM(
prompt: string
): Promise<Result<string, LLMError>> {
return Result.tryPromise(
{
try: async () => {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
}),
});
if (res.status === 429) {
const retryAfter = res.headers.get("retry-after");
throw new RateLimitError({
retryAfterMs: parseInt(retryAfter || "1000")
});
}
if (!res.ok) {
throw new ApiError({
status: res.status,
message: `API error: ${res.status}`
});
}
const data = await res.json();
const content = data.choices?.[0]?.message?.content;
if (!content) {
throw new InvalidResponseError({
reason: "No content in response",
message: "LLM returned empty response"
});
}
return content;
},
catch: (e) => {
if (e instanceof RateLimitError) return e;
if (e instanceof InvalidResponseError) return e;
if (e instanceof ApiError) return e;
return new ApiError({
status: 500,
message: String(e)
});
},
},
{
retry: {
times: 3,
delayMs: 1000,
backoff: "exponential",
shouldRetry: (e) => e._tag === "ApiError" && e.status >= 500,
},
}
);
}
Composing LLM Workflows
Use generator composition for multi-step agentic workflows:
type WorkflowError = LLMError | ValidationError | DatabaseError;
async function agenticWorkflow(
userInput: string
): Promise<Result<string, WorkflowError>> {
return Result.gen(async function* () {
// Step 1: Validate input
const validated = yield* validateInput(userInput);
// Step 2: Generate initial response
const response = yield* Result.await(callLLM(validated.prompt));
// Step 3: Fact-check with retrieval
const facts = yield* Result.await(retrieveFacts(response));
// Step 4: Generate final response with facts
const final = yield* Result.await(
callLLM(`${response}\n\nFacts: ${facts}`)
);
// Step 5: Store in database
yield* Result.await(saveToDatabase(final));
return Result.ok(final);
});
}
// Handle workflow errors with exhaustive matching
const result = await agenticWorkflow(userInput);
result.match({
ok: (output) => console.log("Success:", output),
err: (e) =>
matchError(e, {
RateLimitError: (e) => console.log(`Retry after ${e.retryAfter}ms`),
InvalidResponseError: (e) => console.log(`Bad response: ${e.reason}`),
ApiError: (e) => console.log(`API error: ${e.status}`),
ValidationError: (e) => console.log(`Invalid input: ${e.field}`),
DatabaseError: (e) => console.log(`DB error: ${e.operation}`),
}),
});
Manual Installation
If you prefer not to use the interactive CLI:
# Install package
npm install better-result
# Add source for AI context (optional but recommended)
npx opensrc better-result
# Copy skills manually to your agent's skill folder
# For OpenCode:
cp -r node_modules/better-result/skills/adopt .opencode/skills/better-result-adopt
Source Code Access
The opensrc integration fetches better-result’s full source code to your project:
npx opensrc better-result
This gives AI agents access to:
- Implementation details (
src/result.ts, src/error.ts)
- Test patterns (
src/*.test.ts)
- Type definitions and internal helpers
- Complete documentation
Agents use this context to:
- Understand nuanced behavior
- Match existing code style
- Generate more accurate transformations
Source code is placed in opensrc/ and referenced in AGENTS.md for agent discovery.
Common Agent Workflows
Here are typical commands to use with your AI agent:
Adopt better-result in a module
/skill better-result-adopt
Convert the authentication module to use better-result
Migrate from v1 to v2
/skill better-result-migrate-v2
Migrate all TaggedError classes to v2 API
Refactor error handling
Refactor the API client to use Result.tryPromise with retry logic for network errors
Create new TaggedErrors
Create TaggedError classes for the payment processing module:
PaymentFailedError, InsufficientFundsError, InvalidCardError
Benefits of AI-Assisted Adoption
Using AI agents with better-result skills provides:
- Consistency: Agents apply patterns uniformly across your codebase
- Speed: Bulk transformations complete in minutes, not hours
- Safety: Agents preserve business logic while refactoring error handling
- Learning: See best practices applied to your specific code
- Completeness: Agents update callers, imports, and type signatures automatically
Next Steps