error-handling
affaan-m/ecc
Patterns for robust error handling across TypeScript, Python, and Go with typed errors, retries, and circuit breakers.
What is error-handling?
Provides production-ready error handling patterns for TypeScript, Python, and Go, including typed error classes, result types, API error handlers, and error boundaries. Use this when designing error hierarchies, adding retry logic, reviewing API endpoints for error coverage, or implementing user-facing error messages.
- Define typed error classes and hierarchies with status codes and structured details
- Implement result types (ok/err) for no-throw error handling in expected-failure scenarios
- Build API error handlers that distinguish application errors, validation errors, and unexpected errors
- Create React error boundaries to catch and display component errors gracefully
- Set up FastAPI and Express/Next.js global exception handlers with consistent response formats
- Use sentinel errors and error wrapping in Go to preserve error context across layers
How to install error-handling
npx skills add null --skill error-handlingHow to use error-handling
- 1.Review the core principles section to align your team on error handling philosophy
- 2.Choose the appropriate pattern for your language: typed classes (TS/Python), result types (TS), or sentinel errors (Go)
- 3.Define your application's error hierarchy with domain-specific error types and HTTP status codes
- 4.Implement a global error handler at your API boundary to catch and format errors consistently
- 5.For React apps, wrap components in ErrorBoundary to prevent full-page crashes
- 6.Add logging at error boundaries to capture full context (stack traces, request details) server-side while returning sanitized messages to clients
Use cases
- Designing error types for a new microservice or module
- Adding retry logic and circuit breakers for unreliable external API calls
- Reviewing REST endpoints to ensure all error paths return appropriate status codes and messages
- Implementing user-friendly error messages while logging full context server-side
- Catching unhandled React component errors and displaying fallback UI
- Backend engineers building APIs and services
- Full-stack developers working with TypeScript/React
- Python developers using FastAPI or similar frameworks
- Go developers building HTTP services
- Teams standardizing error handling across multiple languages
error-handling FAQ
Use typed exceptions for unexpected errors and API boundaries; use result types (ok/err) for operations where failure is common and expected (parsing, external calls, validation). Mix both patterns in the same codebase.
Catch errors at the API boundary, log full details server-side, and return a generic message to the client. Only include specific error codes and details that are part of your API contract.
Include structured data useful for debugging or client-side handling: field names for validation errors, resource IDs for not-found errors, retry-after times for rate limits. Avoid stack traces and internal system details.
Wrap external errors with context using error wrapping (Go) or custom error classes (TS/Python). Add retry logic with exponential backoff for transient failures, and circuit breakers to fail fast when a service is down.
Yes, error boundaries catch unhandled errors in the component tree and prevent white-screen crashes. Use them at the page or feature level, and log errors for monitoring.
Full instructions (SKILL.md)
Source of truth, from affaan-m/ecc.
name: error-handling description: Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages. metadata: origin: ECC
Error Handling Patterns
Consistent, robust error handling patterns for production applications.
When to Activate
- Designing error types or exception hierarchies for a new module or service
- Adding retry logic or circuit breakers for unreliable external dependencies
- Reviewing API endpoints for missing error handling
- Implementing user-facing error messages and feedback
- Debugging cascading failures or silent error swallowing
Core Principles
- Fail fast and loudly — surface errors at the boundary where they occur; don't bury them
- Typed errors over string messages — errors are first-class values with structure
- User messages ≠ developer messages — show friendly text to users, log full context server-side
- Never swallow errors silently — every
catchblock must either handle, re-throw, or log - Errors are part of your API contract — document every error code a client may receive
TypeScript / JavaScript
Typed Error Classes
// Define an error hierarchy for your domain
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
public readonly details?: unknown,
) {
super(message)
this.name = this.constructor.name
// Maintain correct prototype chain in transpiled ES5 JavaScript.
// Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)
// to work correctly when extending the built-in Error class.
Object.setPrototypeOf(this, new.target.prototype)
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)
}
}
export class ValidationError extends AppError {
constructor(message: string, details: { field: string; message: string }[]) {
super(message, 'VALIDATION_ERROR', 422, details)
}
}
export class UnauthorizedError extends AppError {
constructor(reason = 'Authentication required') {
super(reason, 'UNAUTHORIZED', 401)
}
}
export class RateLimitError extends AppError {
constructor(public readonly retryAfterMs: number) {
super('Rate limit exceeded', 'RATE_LIMITED', 429)
}
}
Result Pattern (no-throw style)
For operations where failure is expected and common (parsing, external calls):
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E }
function ok<T>(value: T): Result<T> {
return { ok: true, value }
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
// Usage
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findUnique({ where: { id } })
if (!user) return err(new NotFoundError('User', id))
return ok(user)
} catch (e) {
return err(new AppError('Database error', 'DB_ERROR'))
}
}
const result = await fetchUser('abc-123')
if (!result.ok) {
// TypeScript knows result.error here
logger.error('Failed to fetch user', { error: result.error })
return
}
// TypeScript knows result.value here
console.log(result.value.email)
API Error Handler (Next.js / Express)
import { NextRequest, NextResponse } from 'next/server'
function handleApiError(error: unknown): NextResponse {
// Known application error
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
message: error.message,
...(error.details ? { details: error.details } : {}),
},
},
{ status: error.statusCode },
)
}
// Zod validation error
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: {
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
details: error.issues.map(i => ({
field: i.path.join('.'),
message: i.message,
})),
},
},
{ status: 422 },
)
}
// Unexpected error — log details, return generic message
console.error('Unexpected error:', error)
return NextResponse.json(
{ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },
{ status: 500 },
)
}
export async function POST(req: NextRequest) {
try {
// ... handler logic
} catch (error) {
return handleApiError(error)
}
}
React Error Boundary
import { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
fallback: ReactNode
onError?: (error: Error, info: ErrorInfo) => void
children: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info)
console.error('Unhandled React error:', error, info)
}
render() {
if (this.state.hasError) return this.props.fallback
return this.props.children
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong. Please refresh.</p>}>
<MyComponent />
</ErrorBoundary>
Python
Custom Exception Hierarchy
class AppError(Exception):
"""Base application error."""
def __init__(self, message: str, code: str, status_code: int = 500):
super().__init__(message)
self.code = code
self.status_code = status_code
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
class ValidationError(AppError):
def __init__(self, message: str, details: list[dict] | None = None):
super().__init__(message, "VALIDATION_ERROR", 422)
self.details = details or []
FastAPI Global Exception Handler
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={"error": {"code": exc.code, "message": str(exc)}},
)
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
# Log full details, return generic message
logger.exception("Unexpected error", exc_info=exc)
return JSONResponse(
status_code=500,
content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}},
)
Go
Sentinel Errors and Error Wrapping
package domain
import "errors"
// Sentinel errors for type-checking
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Wrap errors with context — never lose the original
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
if err != nil {
return nil, fmt.Errorf("querying user %s: %w", id, err)
}
return user, nil
}
// At the handler level, unwrap to determine response
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
if err != nil {
switch {
case errors.Is(err, domain.ErrNotFound):
writeError(w, http.StatusNotFound, "not_found", err.Error())
case errors.Is(err, domain.ErrUnauthorized):
writeError(w, http.StatusForbidden, "forbidden", "Access denied")
default:
slog.Error("unexpected error", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", "An unexpected error occurred")
}
return
}
writeJSON(w, http.StatusOK, user)
}
Retry with Exponential Backoff
interface RetryOptions {
maxAttempts?: number
baseDelayMs?: number
maxDelayMs?: number
retryIf?: (error: unknown) => boolean
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const {
maxAttempts = 3,
baseDelayMs = 500,
maxDelayMs = 10_000,
retryIf = () => true,
} = options
let lastError: unknown
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
if (attempt === maxAttempts || !retryIf(error)) throw error
const jitter = Math.random() * baseDelayMs
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw lastError
}
// Usage: retry transient network errors, not 4xx
const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
maxAttempts: 3,
retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
})
User-Facing Error Messages
Map error codes to human-readable messages. Keep technical details out of user-visible text.
const USER_ERROR_MESSAGES: Record<string, string> = {
NOT_FOUND: 'The requested item could not be found.',
UNAUTHORIZED: 'Please sign in to continue.',
FORBIDDEN: "You don't have permission to do that.",
VALIDATION_ERROR: 'Please check your input and try again.',
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',
}
export function getUserMessage(code: string): string {
return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
}
Error Handling Checklist
Before merging any code that touches error handling:
- Every
catchblock handles, re-throws, or logs — no silent swallowing - API errors follow the standard envelope
{ error: { code, message } } - User-facing messages contain no stack traces or internal details
- Full error context is logged server-side
- Custom error classes extend a base
AppErrorwith acodefield - Async functions surface errors to callers — no fire-and-forget without fallback
- Retry logic only retries retriable errors (not 4xx client errors)
- React components are wrapped in
ErrorBoundaryfor rendering errors
Related skills
More from affaan-m/ecc and the wider catalog.
eval-harness
Formal evaluation framework for Claude Code sessions using eval-driven development (EDD) principles
everything-claude-code
Development conventions and patterns for the everything-claude-code JavaScript project.
everything-claude-code-conventions
Development conventions and patterns for the everything-claude-code JavaScript project.
evm-token-decimals
Prevent silent decimal mismatch bugs across EVM chains with runtime lookup and chain-aware caching.
exa-search
Neural search via Exa MCP for web, code, companies, and people research.
fal-ai-media
Generate images, videos, and audio with fal.ai models via MCP.