Ai error-handling

Error handling patterns across languages and layers — operational vs programmer errors, retry strategies, circuit breakers, error boundaries, HTTP responses, graceful degradation, and structured logging. Use when designing error strategies, building resilient APIs, or reviewing error management.

install
source · Clone the upstream repo
git clone https://github.com/wpank/ai
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/api/error-handling" ~/.claude/skills/wpank-ai-error-handling && rm -rf "$T"
manifest: skills/api/error-handling/SKILL.md
source content

Error Handling Patterns

Ship resilient software. Handle errors at boundaries, fail fast and loud, never swallow exceptions silently.

Error Handling Philosophy

PrincipleDescription
Fail FastDetect errors early — validate inputs at the boundary, not deep in business logic
Fail LoudErrors must be visible — log them, surface them, alert on them
Handle at BoundariesCatch and translate errors at layer boundaries (controller, middleware, gateway)
Let It CrashFor unrecoverable state, crash and restart (Erlang/OTP philosophy)
Be SpecificCatch specific error types, never bare
catch
or
except
Provide ContextEvery error carries enough context to diagnose without reproducing

Installation

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install error-handling

Error Types

Operational errors — network timeouts, invalid user input, file not found, DB connection lost. Handle gracefully.

Programmer errors

TypeError
, null dereference, assertion failures. Fix the code — don't catch and suppress.

// Operational — handle gracefully
try {
  const data = await fetch('/api/users');
} catch (err) {
  if (err.code === 'ECONNREFUSED') return fallbackData;
  throw err; // re-throw unexpected errors
}

// Programmer — let it crash, fix the bug
const user = null;
user.name; // TypeError — don't try/catch this

Language Patterns

LanguageMechanismAnti-Pattern
JavaScript
try/catch
,
Promise.catch
, Error subclasses
.catch(() => {})
swallowing errors
PythonExceptions, context managers (
with
)
Bare
except:
catching everything
Go
error
returns,
errors.Is/As
,
fmt.Errorf
wrapping
_ = riskyFunction()
ignoring error
Rust
Result<T, E>
,
Option<T>
,
?
operator
.unwrap()
in production code

JavaScript — Error Subclasses

class AppError extends Error {
  constructor(message, code, statusCode, details = {}) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
    this.statusCode = statusCode;
    this.details = details;
    this.isOperational = true;
  }
}

class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource} not found`, 'NOT_FOUND', 404, { resource, id });
  }
}

class ValidationError extends AppError {
  constructor(errors) {
    super('Validation failed', 'VALIDATION_ERROR', 422, { errors });
  }
}

Go — Error Wrapping

func GetUser(id string) (*User, error) {
    row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
    var user User
    if err := row.Scan(&user.ID, &user.Name); err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
        }
        return nil, fmt.Errorf("querying user %s: %w", id, err)
    }
    return &user, nil
}

Error Boundaries

Express Error Middleware

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const response = {
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: err.isOperational ? err.message : 'Something went wrong',
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
      requestId: req.id,
    },
  };

  logger.error('Request failed', {
    err, requestId: req.id, method: req.method, path: req.path,
  });

  res.status(statusCode).json(response);
});

React Error Boundary

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <h2>Something went wrong</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => queryClient.clear()}>
  <App />
</ErrorBoundary>

Retry Patterns

PatternWhen to UseConfig
Exponential BackoffTransient failures (network, 503)Base 1s, max 30s, factor 2x
Backoff + JitterMultiple clients retryingRandom ±30% on each delay
Circuit BreakerDownstream service failing repeatedlyOpen after 5 failures, half-open after 30s
BulkheadIsolate failures to prevent cascadeLimit concurrent calls per service
TimeoutPrevent indefinite hangsConnect 5s, read 30s, total 60s

Exponential Backoff with Jitter

async function withRetry(fn, { maxRetries = 3, baseDelay = 1000, maxDelay = 30000 } = {}) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxRetries || !isRetryable(err)) throw err;
      const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
      const jitter = delay * (0.7 + Math.random() * 0.6);
      await new Promise((r) => setTimeout(r, jitter));
    }
  }
}

function isRetryable(err) {
  return [408, 429, 500, 502, 503, 504].includes(err.statusCode) || err.code === 'ECONNRESET';
}

Circuit Breaker

class CircuitBreaker {
  constructor({ threshold = 5, resetTimeout = 30000 } = {}) {
    this.state = 'CLOSED';       // CLOSED → OPEN → HALF_OPEN → CLOSED
    this.failureCount = 0;
    this.threshold = threshold;
    this.resetTimeout = resetTimeout;
    this.nextAttempt = 0;
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) throw new Error('Circuit is OPEN');
      this.state = 'HALF_OPEN';
    }
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; }
  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }
}

HTTP Error Responses

StatusNameWhen to Use
400Bad RequestMalformed syntax, invalid JSON
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but insufficient permissions
404Not FoundResource does not exist
409ConflictRequest conflicts with current state
422Unprocessable EntityValid syntax but semantic errors
429Too Many RequestsRate limit exceeded (include
Retry-After
)
500Internal Server ErrorUnexpected server failure
502Bad GatewayUpstream returned invalid response
503Service UnavailableTemporarily overloaded or maintenance

Standard Error Envelope

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request body contains invalid fields.",
    "details": [
      { "field": "email", "message": "Must be a valid email address" }
    ],
    "requestId": "req_abc123xyz"
  }
}

Graceful Degradation

StrategyExample
Fallback valuesShow cached avatar when image service is down
Feature flagsDisable unstable recommendation engine
Cached responsesServe stale data with
X-Cache: STALE
header
Partial responseReturn available data with
warnings
array
async function getProductPage(productId) {
  const product = await productService.get(productId); // critical — propagate errors

  const [reviews, recommendations] = await Promise.allSettled([
    reviewService.getForProduct(productId),
    recommendationService.getForProduct(productId),
  ]);

  return {
    product,
    reviews: reviews.status === 'fulfilled' ? reviews.value : [],
    recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
    warnings: [reviews, recommendations]
      .filter((r) => r.status === 'rejected')
      .map((r) => ({ service: 'degraded', reason: r.reason.message })),
  };
}

Logging & Monitoring

PracticeImplementation
Structured loggingJSON:
level
,
message
,
error
,
requestId
,
userId
,
timestamp
Error trackingSentry, Datadog, Bugsnag — automatic capture with source maps
Alert thresholdsError rate > 1%, P99 latency > 2s, 5xx spike
Correlation IDsPass
requestId
through all service calls
Log levels
error
= needs attention,
warn
= degraded,
info
= normal,
debug
= dev

Anti-Patterns

Anti-PatternFix
Swallowing errors
catch (e) {}
Log and re-throw, or handle explicitly
Generic catch-all at every levelCatch specific types, let unexpected errors bubble
Error as control flowUse conditionals, return values, or option types
Stringly-typed errors
throw "wrong"
Throw
Error
objects with codes and context
Logging and throwingLog at the boundary only, or wrap and re-throw
Catch-and-return-nullReturn
Result
type, throw, or return error object
Ignoring Promise rejectionsAlways
await
or attach
.catch()
Exposing internalsSanitize responses; log details server-side only

NEVER Do

  1. NEVER swallow errors silently
    catch (e) {}
    hides bugs and causes silent data corruption
  2. NEVER expose stack traces, SQL errors, or file paths in API responses — log details server-side only
  3. NEVER use string throws
    throw 'error'
    has no stack trace, no type, no context
  4. NEVER catch and return null without explanation — callers have no idea why the operation failed
  5. NEVER ignore unhandled Promise rejections — always
    await
    or attach
    .catch()
  6. NEVER cache error responses — 5xx and transient errors must not be cached and re-served
  7. NEVER use exceptions for normal control flow — exceptions are for exceptional conditions
  8. NEVER return generic "Something went wrong" without logging the real error — always log the full error server-side with request context