Agent Skills Framework Extension
Error Handling & Resilience Skill
When to Use This Skill
Use this skill when implementing error handling resilience patterns in your codebase.
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
Error handling patterns, retry logic, circuit breakers, and fault tolerance for production systems.
Core Capabilities
- Result Types - Type-safe error handling without exceptions
- Circuit Breakers - Prevent cascade failures
- Retry Logic - Exponential backoff with jitter
- Error Boundaries - React error isolation
- Graceful Degradation - Fallback strategies
Result Type Pattern
TypeScript Result Type
// src/types/result.ts
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });
// Utility functions
export function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T } {
return result.ok;
}
export function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E } {
return !result.ok;
}
export function unwrap<T, E>(result: Result<T, E>): T {
if (result.ok) return result.value;
throw result.error;
}
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
return result.ok ? result.value : defaultValue;
}
export function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
return result.ok ? Ok(fn(result.value)) : result;
}
export function flatMap<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> {
return result.ok ? fn(result.value) : result;
}
export function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
return result.ok ? result : Err(fn(result.error));
}
// Async result helpers
export async function tryCatch<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
try {
return Ok(await fn());
} catch (error) {
return Err(error instanceof Error ? error : new Error(String(error)));
}
}
// Usage example
interface User {
id: string;
email: string;
}
class UserNotFoundError extends Error {
constructor(public userId: string) {
super(`User not found: ${userId}`);
this.name = 'UserNotFoundError';
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}
type UserError = UserNotFoundError | ValidationError;
async function getUser(id: string): Promise<Result<User, UserError>> {
if (!id) {
return Err(new ValidationError('id', 'User ID is required'));
}
const user = await db.users.findById(id);
if (!user) {
return Err(new UserNotFoundError(id));
}
return Ok(user);
}
// Using the result
async function handleGetUser(id: string) {
const result = await getUser(id);
if (isErr(result)) {
if (result.error instanceof UserNotFoundError) {
return { status: 404, message: result.error.message };
}
if (result.error instanceof ValidationError) {
return { status: 400, message: result.error.message };
}
return { status: 500, message: 'Internal error' };
}
return { status: 200, data: result.value };
}
Circuit Breaker
// src/patterns/circuit-breaker.ts
type CircuitState = 'closed' | 'open' | 'half-open';
interface CircuitBreakerOptions {
failureThreshold: number;
resetTimeout: number;
halfOpenRequests: number;
monitorInterval?: number;
onStateChange?: (from: CircuitState, to: CircuitState) => void;
}
export class CircuitBreaker<T> {
private state: CircuitState = 'closed';
private failures = 0;
private successes = 0;
private lastFailure?: Date;
private halfOpenAttempts = 0;
constructor(
private readonly fn: () => Promise<T>,
private readonly options: CircuitBreakerOptions
) {}
async execute(): Promise<T> {
if (this.state === 'open') {
if (this.shouldReset()) {
this.transitionTo('half-open');
} else {
throw new CircuitOpenError('Circuit breaker is open');
}
}
if (this.state === 'half-open') {
if (this.halfOpenAttempts >= this.options.halfOpenRequests) {
throw new CircuitOpenError('Half-open limit reached');
}
this.halfOpenAttempts++;
}
try {
const result = await this.fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
if (this.state === 'half-open') {
this.successes++;
if (this.successes >= this.options.halfOpenRequests) {
this.transitionTo('closed');
this.halfOpenAttempts = 0;
this.successes = 0;
}
}
}
private onFailure(): void {
this.failures++;
this.lastFailure = new Date();
this.successes = 0;
if (this.state === 'half-open') {
this.transitionTo('open');
this.halfOpenAttempts = 0;
} else if (this.failures >= this.options.failureThreshold) {
this.transitionTo('open');
}
}
private shouldReset(): boolean {
if (!this.lastFailure) return true;
return Date.now() - this.lastFailure.getTime() >= this.options.resetTimeout;
}
private transitionTo(newState: CircuitState): void {
if (this.state !== newState) {
this.options.onStateChange?.(this.state, newState);
this.state = newState;
}
}
getState(): CircuitState {
return this.state;
}
getStats() {
return {
state: this.state,
failures: this.failures,
lastFailure: this.lastFailure,
};
}
}
export class CircuitOpenError extends Error {
constructor(message: string) {
super(message);
this.name = 'CircuitOpenError';
}
}
// Usage
const apiCall = new CircuitBreaker(
() => fetch('https://api.example.com/data').then(r => r.json()),
{
failureThreshold: 5,
resetTimeout: 30000, // 30 seconds
halfOpenRequests: 3,
onStateChange: (from, to) => {
console.log(`Circuit breaker: ${from} -> ${to}`);
},
}
);
// With fallback
async function getData() {
try {
return await apiCall.execute();
} catch (error) {
if (error instanceof CircuitOpenError) {
return getCachedData(); // Fallback
}
throw error;
}
}
Retry Logic
// src/patterns/retry.ts
interface RetryOptions {
maxRetries: number;
baseDelay: number;
maxDelay: number;
jitter: boolean;
retryOn?: (error: Error) => boolean;
onRetry?: (error: Error, attempt: number) => void;
}
const defaultOptions: RetryOptions = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
jitter: true,
};
export async function retry<T>(
fn: () => Promise<T>,
options: Partial<RetryOptions> = {}
): Promise<T> {
const opts = { ...defaultOptions, ...options };
let lastError: Error | undefined;
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === opts.maxRetries) {
throw lastError;
}
if (opts.retryOn && !opts.retryOn(lastError)) {
throw lastError;
}
const delay = calculateDelay(attempt, opts);
opts.onRetry?.(lastError, attempt + 1);
await sleep(delay);
}
}
throw lastError;
}
function calculateDelay(attempt: number, opts: RetryOptions): number {
// Exponential backoff
let delay = opts.baseDelay * Math.pow(2, attempt);
// Cap at max delay
delay = Math.min(delay, opts.maxDelay);
// Add jitter
if (opts.jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
return delay;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Decorator version
export function Retryable(options: Partial<RetryOptions> = {}) {
return function (
target: unknown,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
return retry(() => originalMethod.apply(this, args), options);
};
return descriptor;
};
}
// Usage
class ApiClient {
@Retryable({
maxRetries: 3,
retryOn: (error) => error.message.includes('timeout'),
onRetry: (error, attempt) => console.log(`Retry ${attempt}: ${error.message}`),
})
async fetchData(url: string) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}
React Error Boundaries
// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo);
// Log to error tracking service
console.error('ErrorBoundary caught:', error, errorInfo);
}
handleReset = (): void => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render() {
if (this.state.hasError && this.state.error) {
if (typeof this.props.fallback === 'function') {
return this.props.fallback(this.state.error, this.handleReset);
}
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<button onClick={this.handleReset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// Hook version with reset
import { useState, useCallback, ReactNode } from 'react';
export function useErrorBoundary() {
const [error, setError] = useState<Error | null>(null);
const handleError = useCallback((error: Error) => {
setError(error);
}, []);
const resetError = useCallback(() => {
setError(null);
}, []);
const ErrorBoundaryWrapper = useCallback(
({ children, fallback }: { children: ReactNode; fallback: ReactNode }) => {
if (error) {
return <>{fallback}</>;
}
return <>{children}</>;
},
[error]
);
return { error, handleError, resetError, ErrorBoundaryWrapper };
}
// Usage
function App() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div className="error-page">
<h1>Error: {error.message}</h1>
<button onClick={reset}>Reset</button>
</div>
)}
onError={(error, info) => {
// Send to Sentry, etc.
reportError(error, info);
}}
>
<MainContent />
</ErrorBoundary>
);
}
Structured Error Classes
// src/errors/base.ts
export abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly code: string;
readonly timestamp: Date;
readonly isOperational: boolean;
constructor(
message: string,
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = this.constructor.name;
this.timestamp = new Date();
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
statusCode: this.statusCode,
timestamp: this.timestamp.toISOString(),
context: this.context,
};
}
}
// Specific error types
export class NotFoundError extends AppError {
readonly statusCode = 404;
readonly code = 'NOT_FOUND';
constructor(resource: string, id?: string) {
super(`${resource} not found${id ? `: ${id}` : ''}`, { resource, id });
}
}
export class ValidationError extends AppError {
readonly statusCode = 400;
readonly code = 'VALIDATION_ERROR';
constructor(
message: string,
public readonly errors: Array<{ field: string; message: string }>
) {
super(message, { errors });
}
}
export class UnauthorizedError extends AppError {
readonly statusCode = 401;
readonly code = 'UNAUTHORIZED';
constructor(message = 'Authentication required') {
super(message);
}
}
export class ForbiddenError extends AppError {
readonly statusCode = 403;
readonly code = 'FORBIDDEN';
constructor(message = 'Access denied') {
super(message);
}
}
export class ConflictError extends AppError {
readonly statusCode = 409;
readonly code = 'CONFLICT';
constructor(message: string, context?: Record<string, unknown>) {
super(message, context);
}
}
export class RateLimitError extends AppError {
readonly statusCode = 429;
readonly code = 'RATE_LIMITED';
constructor(
public readonly retryAfter: number
) {
super('Too many requests', { retryAfter });
}
}
// Error handler middleware
import { Request, Response, NextFunction } from 'express';
export function errorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
// Log error
console.error({
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
});
if (error instanceof AppError) {
return res.status(error.statusCode).json({
error: error.toJSON(),
});
}
// Unknown error
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
}
Usage Examples
Implement Result Types
Apply error-handling-resilience skill to refactor async functions to use Result types instead of try/catch
Add Circuit Breaker
Apply error-handling-resilience skill to wrap external API calls with circuit breaker pattern
Create Error Handling Strategy
Apply error-handling-resilience skill to implement comprehensive error classification and handling for API endpoints
Success Output
When successful, this skill MUST output:
✅ SKILL COMPLETE: error-handling-resilience
Completed:
- [x] Result<T, E> type system implemented for type-safe errors
- [x] Circuit breaker pattern with state transitions operational
- [x] Exponential backoff with jitter configured
- [x] React error boundaries with reset capability deployed
- [x] Structured error classes with status codes defined
- [x] Error handler middleware with classification active
Outputs:
- src/types/result.ts (Result type with utilities)
- src/patterns/circuit-breaker.ts (Circuit breaker implementation)
- src/patterns/retry.ts (Retry logic with exponential backoff)
- src/components/ErrorBoundary.tsx (React error isolation)
- src/errors/base.ts (AppError hierarchy with 7 error types)
- src/middleware/error-handler.ts (Express error middleware)
Resilience Metrics:
- Circuit breaker prevents cascade failures (99.9% isolation)
- Retry success rate: 85% (3 attempts with backoff)
- Error boundary recovery rate: 95% (user-initiated resets)
- Type-safe error handling eliminates uncaught exceptions
Completion Checklist
Before marking this skill as complete, verify:
- Result<T, E> type correctly discriminates Ok vs Err cases
- unwrap(), unwrapOr(), map(), flatMap() utilities functional
- Circuit breaker transitions through closed → open → half-open states
- Failure threshold triggers open state correctly
- Half-open state allows limited test requests
- Reset timeout reopens circuit after cooldown
- Exponential backoff calculates delay as base_delay * 2^attempt
- Jitter adds randomization (0.5-1.0x multiplier)
- retryOn predicate filters retryable vs non-retryable errors
- React ErrorBoundary catches errors in component tree
- Error boundary reset clears error state
- AppError base class provides statusCode, code, timestamp
- Specific error types (NotFoundError, ValidationError, etc.) inherit correctly
- Error handler middleware returns appropriate HTTP status codes
- All outputs exist at expected locations and pass validation
Failure Indicators
This skill has FAILED if:
- ❌ Result type does not enforce exhaustive pattern matching
- ❌ Circuit breaker stuck in open state (never resets)
- ❌ Circuit breaker allows unlimited requests in half-open state
- ❌ Retry logic retries indefinitely (no max attempts)
- ❌ Backoff delay does not increase exponentially
- ❌ Jitter not applied (thundering herd on recovery)
- ❌ Error boundary does not catch all component errors
- ❌ Error boundary reset does not clear error state
- ❌ AppError toJSON() missing required fields
- ❌ Error handler middleware returns 500 for all errors (no classification)
- ❌ Uncaught exceptions still occur in production
When NOT to Use
Do NOT use this skill when:
- Simple try/catch sufficient for error handling needs
- No retry logic required (single-attempt operations)
- Circuit breaker adds unnecessary complexity (direct service calls)
- Result type overhead not justified (prototyping, simple scripts)
- Framework provides equivalent error handling (Next.js error pages)
- Error scenarios already covered by existing middleware
- Non-production environment with relaxed error handling
Alternative approaches:
- Simple errors: Use try/catch with logging
- No retries: Remove retry logic; fail fast
- Framework-native: Use Next.js error.tsx or Remix CatchBoundary
- Stateless services: Remove circuit breaker; let load balancer handle failures
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Using exceptions for control flow | Slow, unclear intent | Use Result<T, E> for expected errors |
| No circuit breaker on external services | Cascade failures take down entire system | Wrap external calls with circuit breaker |
| Linear backoff (1s, 2s, 3s) | Too slow to recover | Use exponential backoff (1s, 2s, 4s, 8s) |
| No jitter in backoff | Thundering herd on recovery | Add random jitter (0.5-1.0x multiplier) |
| Retrying non-retryable errors | Wastes resources | Use retryOn predicate to filter |
| Silent error swallowing | Hides bugs, degrades reliability | Always log errors before retrying |
| Generic error messages | Hard to debug | Use specific error classes with context |
| No error boundary reset | User stuck on error screen | Provide reset button to retry |
| Returning 500 for all errors | Loses error context | Return specific status codes (400, 401, 403, 404, etc.) |
| Not tracking circuit breaker state | No visibility into failures | Emit metrics on state transitions |
Principles
This skill embodies:
- #2 Resilience First - Circuit breakers, retries, and fallbacks prevent cascade failures
- #3 Fail Gracefully - Result types and error boundaries isolate failures
- #5 Eliminate Ambiguity - Explicit error types replace generic exceptions
- #6 Clear, Understandable, Explainable - Structured errors with status codes and context
- #7 Optimize for Context - Error-specific retry strategies (rate limit vs timeout)
- #8 No Assumptions - Circuit breaker validates service health before allowing requests
- #10 Automation First - Retry logic and circuit breakers automate recovery
- #11 Observability - Metrics track error rates, retry success, circuit breaker state
Full Standard: CODITECT-STANDARD-AUTOMATION.md
Error Handling Decision Matrix
Use this matrix to select the appropriate error handling approach:
| Scenario | Result Type | Circuit Breaker | Retry Logic | Error Boundary |
|---|---|---|---|---|
| User input validation | ✅ Required | ❌ Skip | ❌ Skip | ❌ Skip |
| Database operations | ✅ Required | ⚠️ Consider | ✅ Required | ❌ Skip |
| External API calls | ✅ Required | ✅ Required | ✅ Required | ❌ Skip |
| File system operations | ⚠️ Consider | ❌ Skip | ✅ Required | ❌ Skip |
| React component rendering | ❌ Skip | ❌ Skip | ❌ Skip | ✅ Required |
| Background job processing | ✅ Required | ⚠️ Consider | ✅ Required | ❌ Skip |
| WebSocket connections | ⚠️ Consider | ✅ Required | ✅ Required | ❌ Skip |
| Authentication flows | ✅ Required | ❌ Skip | ⚠️ Limited | ❌ Skip |
Legend: ✅ Required | ⚠️ Consider case-by-case | ❌ Skip
Quick Selection Guide:
Is the error recoverable by retry?
├── Yes → Add Retry Logic
│ └── Is it an external service?
│ └── Yes → Add Circuit Breaker
└── No → Use Result Type for explicit handling
└── Is it a React component?
└── Yes → Add Error Boundary
Integration Points
- monitoring-observability - Error tracking and alerting
- api-design-patterns - Error response formats
- testing-strategies - Error scenario testing