Skip to main content

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

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Error handling patterns, retry logic, circuit breakers, and fault tolerance for production systems.

Core Capabilities

  1. Result Types - Type-safe error handling without exceptions
  2. Circuit Breakers - Prevent cascade failures
  3. Retry Logic - Exponential backoff with jitter
  4. Error Boundaries - React error isolation
  5. 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-PatternProblemSolution
Using exceptions for control flowSlow, unclear intentUse Result<T, E> for expected errors
No circuit breaker on external servicesCascade failures take down entire systemWrap external calls with circuit breaker
Linear backoff (1s, 2s, 3s)Too slow to recoverUse exponential backoff (1s, 2s, 4s, 8s)
No jitter in backoffThundering herd on recoveryAdd random jitter (0.5-1.0x multiplier)
Retrying non-retryable errorsWastes resourcesUse retryOn predicate to filter
Silent error swallowingHides bugs, degrades reliabilityAlways log errors before retrying
Generic error messagesHard to debugUse specific error classes with context
No error boundary resetUser stuck on error screenProvide reset button to retry
Returning 500 for all errorsLoses error contextReturn specific status codes (400, 401, 403, 404, etc.)
Not tracking circuit breaker stateNo visibility into failuresEmit 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:

ScenarioResult TypeCircuit BreakerRetry LogicError 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