Error Handling

affaan-m/ECC/skills/error-handling

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages. Use when designing error types, retries, circuit breakers, or user-facing failure messages in TypeScript, Python, or Go.

Instructions onlySoftware Development
AI-generated overview

Reference patterns for robust error handling in TypeScript, Python, and Go, covering typed errors, retries, and user-facing messages.

What it does
Provides guidance and code patterns for consistent error handling across TypeScript, Python, and Go. It covers typed error hierarchies, the Result pattern, API error handlers, React error boundaries, FastAPI exception handlers, Go sentinel errors and wrapping, retry with exponential backoff, and user-facing error messages. It also includes a pre-merge checklist for error handling.
When to use it
Use when designing error types or exception hierarchies, adding retry logic or circuit breakers, reviewing API endpoints for missing error handling, or writing user-facing failure messages. It targets TypeScript, Python, and Go codebases.
Requirements
No scripts or tools are required; it is an instructions-only reference. The patterns assume TypeScript/JavaScript, Python, and Go projects, with examples referencing libraries such as Next.js, Express, Zod, React, and FastAPI.

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

  1. Fail fast and loudly — surface errors at the boundary where they occur; don't bury them
  2. Typed errors over string messages — errors are first-class values with structure
  3. User messages ≠ developer messages — show friendly text to users, log full context server-side
  4. Never swallow errors silently — every catch block must either handle, re-throw, or log
  5. Errors are part of your API contract — document every error code a client may receive

TypeScript / JavaScript

Typed Error Classes

typescript
// Define an error hierarchy for your domainexport 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):

typescript
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 }}
// Usageasync 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 hereconsole.log(result.value.email)

API Error Handler (Next.js / Express)

typescript
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

typescript
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

python
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

python
from fastapi import FastAPI, Requestfrom 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

go
package domain
import "errors"
// Sentinel errors for type-checkingvar (    ErrNotFound    = errors.New("not found")    ErrUnauthorized = errors.New("unauthorized")    ErrConflict     = errors.New("conflict"))
// Wrap errors with context — never lose the originalfunc (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 responsefunc (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

typescript
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 4xxconst 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.

typescript
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 catch block 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 AppError with a code field
  • 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 ErrorBoundary for rendering errors

Source and attribution

Source:affaan-m/ECCinskills/error-handlingat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal