error-handling

Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error…

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill error-handling
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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 &#x26;&#x26; 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 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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card