Never throw for expected failures. Use Result<T, E> types with explicit error handling and workflow composition.
Enables railway-oriented programming with Result<T, E> types for explicit error handling. Use when composing workflows with `createWorkflow()` to automatically short-circuit on errors and maintain type-safe error unions across your application.
/plugin marketplace add jagreehal/jagreehal-claude-skills/plugin install jagreehal-claude-skills@jagreehal-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Exceptions are invisible, bypass composition, and conflate different failures. Return Result<T, E> instead.
// WRONG - Signature lies
async function getUser(args): Promise<User> {
const user = await deps.db.findUser(args.userId);
if (!user) throw new Error('User not found'); // Hidden!
return user;
}
// CORRECT - Signature tells the truth
async function getUser(args, deps): Promise<Result<User, 'NOT_FOUND' | 'DB_ERROR'>> {
try {
const user = await deps.db.findUser(args.userId);
return user ? ok(user) : err('NOT_FOUND');
} catch {
return err('DB_ERROR');
}
}
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
type AsyncResult<T, E> = Promise<Result<T, E>>;
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
async function getUser(
args: { userId: string },
deps: GetUserDeps
): Promise<Result<User, 'NOT_FOUND' | 'DB_ERROR'>> {
try {
const user = await deps.db.findUser(args.userId);
if (!user) return err('NOT_FOUND');
return ok(user);
} catch {
return err('DB_ERROR');
}
}
Avoid verbose if-checking with railway-oriented programming:
import { createWorkflow } from '@jagreehal/workflow';
// Declare dependencies -> error union computed automatically
const loadUserData = createWorkflow({ getUser, getPosts, enrichUser });
const result = await loadUserData(async (step) => {
const user = await step(() => getUser({ userId }, deps));
const posts = await step(() => getPosts({ userId: user.id }, deps));
const enriched = await step(() => enrichUser({ user, posts }, deps));
return { user: enriched };
});
// result: Result<{ user: EnrichedUser }, 'NOT_FOUND' | 'DB_ERROR' | 'FETCH_ERROR' | ...>
The step() function:
ok results and continues on happy patherr, immediately short-circuits and skips remaining stepsBridge between throwing code and Result pipeline:
const workflow = createWorkflow({ getUser });
const result = await workflow(async (step) => {
const user = await step(() => getUser({ userId }, deps));
// Throwing function: use step.try() with error mapping
const config = await step.try(
() => JSON.parse(user.configJson),
{ error: 'INVALID_CONFIG' as const }
);
return { user, config };
});
step(): For functions that already return Result (your code)step.try(): For functions that throw (third-party, built-in)step.fromResult(): For Result-returning functions where you need to map errorsFor Result-returning functions: Use step.fromResult() to preserve typed errors:
// callProvider returns Result<Response, ProviderError>
const callProvider = async (input: string): AsyncResult<Response, ProviderError> => { ... };
const response = await step.fromResult(
() => callProvider(input),
{
onError: (e) => ({
type: 'PROVIDER_FAILED' as const,
provider: e.provider, // TypeScript knows e is ProviderError
code: e.code,
})
}
);
const errorToStatus: Record<string, number> = {
NOT_FOUND: 404,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
VALIDATION_FAILED: 400,
CONFLICT: 409,
};
function resultToResponse<T, E extends string>(
result: Result<T, E>,
res: Response
): Response {
if (result.ok) {
return res.status(200).json(result.value);
}
const status = errorToStatus[result.error] ?? 500;
return res.status(status).json({
error: result.error,
code: result.error,
});
}
// Handler becomes simple
app.get('/users/:id', async (req, res) => {
const result = await getUser({ userId: req.params.id }, deps);
return resultToResponse(result, res);
});
TypeScript enforces handling all error cases:
if (!result.ok) {
switch (result.error) {
case 'NOT_FOUND':
return res.status(404).json({ error: 'User not found' });
case 'DB_ERROR':
case 'FETCH_ERROR':
return res.status(500).json({ error: 'Internal error' });
// TypeScript will error if you miss a case!
}
}
type AppError = 'NOT_FOUND' | 'UNAUTHORIZED' | 'DB_ERROR';
type AppError =
| { type: 'NOT_FOUND'; resource: string }
| { type: 'VALIDATION'; field: string; message: string }
| { type: 'DB_ERROR'; query: string };
const Errors = {
NOT_FOUND: 'NOT_FOUND',
DB_ERROR: 'DB_ERROR',
} as const;
type AppError = (typeof Errors)[keyof typeof Errors];
return err(Errors.NOT_FOUND); // Runtime value available
As applications grow, error unions become unwieldy:
// This becomes a "Type Wall"
type AllErrors =
| 'NOT_FOUND'
| 'DB_ERROR'
| 'DB_CONNECTION_FAILED'
| 'DB_TIMEOUT'
| 'FETCH_ERROR'
| 'HTTP_TIMEOUT'
| 'RATE_LIMITED'
| 'CIRCUIT_OPEN'
| 'VALIDATION_FAILED'
// ... 20 more errors
Solution: Group related errors into categories:
// Group by domain
type DatabaseError = 'DB_ERROR' | 'DB_CONNECTION_FAILED' | 'DB_TIMEOUT';
type NetworkError = 'FETCH_ERROR' | 'HTTP_TIMEOUT' | 'RATE_LIMITED';
type BusinessError = 'NOT_FOUND' | 'VALIDATION_FAILED' | 'UNAUTHORIZED';
type AppError = DatabaseError | NetworkError | BusinessError;
// Or use discriminated unions for richer context
type AppError =
| { type: 'DATABASE'; code: 'CONNECTION_FAILED' | 'TIMEOUT' | 'QUERY_FAILED' }
| { type: 'NETWORK'; code: 'TIMEOUT' | 'RATE_LIMITED' | 'UNREACHABLE' }
| { type: 'BUSINESS'; code: 'NOT_FOUND' | 'VALIDATION_FAILED' };
This keeps error types manageable while preserving type safety.
Throw only for:
// Good: throw for impossible states
if (!user) throw new Error('Unreachable: user should exist after insert');
asserts for Type NarrowingThe asserts keyword creates runtime checks that also narrow types:
// Assert function: throws if condition fails, narrows type if succeeds
function assertUser(user: User | null): asserts user is User {
if (!user) throw new Error('Invariant violated: user must exist');
}
function assertDefined<T>(value: T | undefined, name: string): asserts value is T {
if (value === undefined) throw new Error(`${name} must be defined`);
}
// Usage: TypeScript narrows the type after the assertion
const user = await deps.db.findUser(userId);
assertUser(user); // Throws if null
// TypeScript now knows `user` is `User`, not `User | null`
console.log(user.name); // Safe access
When to use asserts:
Don't use for: Normal business logic failures (use Result instead)
| Situation | Use |
|---|---|
| Domain failure (not found, validation) | Result |
| Infrastructure failure (recoverable) | Result |
| Programmer error | throw |
| Corrupted state | throw |
Handlers / Routes
-> map Result -> HTTP response
Business Logic
-> createWorkflow({ ... })(async (step) => { ... })
Core Functions
-> fn(args, deps): Result<T, E>
Infrastructure
-> catch exceptions, return Results
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.