awesome-error-standards · git:20261010.3b37a6b · 2026-10-10 · sha256 f8f45971bd93d295

awesome-error-standards git:20261010.3b37a6bA

Immutable. This exact content never changes and is served at /api/v1/blob/f8f45971bd93d295.

---
name: awesome-error-standards
description: "Applies consistent error handling: typed errors, operational vs programmer errors, API error envelopes, HTTP status mapping, user-facing messages. Use when adding or refactoring error handling or designing an error contract."
license: MIT
metadata:
  author: Khasky
  tags: ["error-handling", "reliability", "api", "patterns"]
  documentation: "https://github.com/khasky/awesome-agent-skills/tree/main/skills/awesome-error-standards"
---

# Error Handling Patterns

Apply consistent patterns for throwing, catching, logging, and surfacing errors so failures are predictable and debuggable without leaking internals.

## When to Activate

- Implementing or refactoring error handling in an API or app
- User asks for "error handling", "how to handle errors", or "consistent errors"
- Reviewing code for proper failure handling
- Defining or documenting error contract for a service or API

## Core Principles

- Operational vs programmer errors — Classify first. Operational failures (network timeout, invalid input, missing record) are expected: handle, retry, or surface them. Programmer errors (undefined access, broken invariant) are bugs: crash loudly or restart the process rather than limping on with corrupted state.
- Make the bug impossible, not just fixed — A single-point fix stops one path; defense in depth stops the class. Prefer prevention left of production, in order: (1) make invalid states unrepresentable in the type system (discriminated unions, branded/opaque types) so bad states won't compile; (2) validate at the boundary; (3) runtime guards; (4) error boundaries. For a high-value invariant, guard it at more than one layer and test that bypassing an outer layer still gets caught by an inner one (deliberately skip Layer 1, verify Layer 2 catches it; confirm mocks don't silently disable a validation).
  Rate every fix on a ladder of rungs. Control: the mistake cannot be made (the type, the API shape, the removed option). Warning: it announces itself at the moment it is made (a compile error, a failed validation, a lint rule). Detection: it is found later (tests, monitoring, alerts). The zero rung is a comment, a doc line or "be careful"; it is not a fix and does not count. Monitoring is not prevention: an alert on a bug that already reached users has moved the bug, not stopped it. A fix report names what it made impossible and what it deliberately left possible.
- Fail at startup, not in production — Validate all required config, env vars, and connections at init and refuse to boot on failure, rather than discovering a missing value on the first request.
- Fail fast — Validate inputs and preconditions early; throw or return errors with clear messages. Do not continue with invalid state.
- Do not swallow — Avoid empty catch or broad catch that ignores; log and/or rethrow or return a result type. Propagate with context.
- Structured errors — Use a consistent shape (e.g. code, message, details) for API errors and for logs. Typed error classes or codes help callers handle by type.
- User vs developer — User-facing messages are safe and actionable; developer-facing detail (stack, internal message, request id) in logs or debug only. Never expose stack traces or SQL to end users.

## Work Process

1. Identify boundaries — Where do errors originate (validation, DB, external API, auth)? Where are they handled (route handler, middleware, top-level)? Ensure every layer either handles or propagates with context.
2. Define error shape — For API: HTTP status + body envelope (code, message, optional details). For code: error class or result type. Match existing project pattern if present.
3. Map errors to HTTP (for APIs) — Validation → 400 or 422; auth → 401; authz → 403; not found → 404; conflict → 409; server → 500. Do not return 200 with `success: false` for errors. Pick ONE style for validation failures (400 vs 422), document it, and apply it across every endpoint — mixed behavior breaks generated clients and callers' error branching.
4. Log before respond — Log error with context (request id, user id if safe, operation) at appropriate level (error/warn). Then return user-safe response.
5. Validate and sanitize — Use schema (Zod, Pydantic, etc.) at API boundary; return field-level validation errors in consistent format.

## API Error Response

Standard envelope:

```json
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address", "code": "invalid_format" }
    ]
  }
}
```

- code — Machine-readable (e.g. `validation_error`, `not_found`, `rate_limit_exceeded`). Clients can switch on this.
- message — Human-readable, safe to show to user. No stack traces or internal paths.
- details — Optional; for validation, list field-level errors. Omit for generic 500.
- request_id / trace_id — Optional in envelope or headers for support; do not expose internals.

HTTP status: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests, 500 Internal Server Error. Use semantically; do not use 200 for errors.

## Retrying failed calls (client side)

Classify every failure by whether a retry can help — retrying a non-retryable error just wastes time and can double effects:

- Never retry (fix the request): 400, 401, 403, 404, 415, most 422 — the input or auth is wrong, retrying won't change it.
- Retry after modifying the request: 402 (top up / change plan), some 422 (adjust payload). Not a blind retry.
- Retry with backoff: 429, 500, 502, 503, 504 — transient. Cap attempts (~5) and total wait (~30s); use exponential backoff with jitter to avoid thundering herds.

Rules:
- Honor the server's signal — respect `Retry-After` and `X-RateLimit-Reset`/`X-RateLimit-*` headers over your own timer when present.
- Idempotency-Key on retried state-mutating requests — a POST/PATCH that charges, creates, or sends can succeed on the server while the response is lost; a naive retry double-charges. Send a stable idempotency key so the server dedupes.
- Streaming (SSE/websocket) — errors arrive as an in-stream event (an `error` event or a terminal frame), not as an HTTP status, because the status was already 200 when the stream opened. Handle the stream's error channel explicitly, not just the initial response code.

## Code Patterns

Typed errors (recommended):

```typescript
class NotFoundError extends Error {
  constructor(resource: string) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
  }
}
// Caller: catch (e) { if (e instanceof NotFoundError) return res.status(404).json(...); }
```

Layered handling: Lower layers throw or return Result; route handler or middleware catches and maps to HTTP + log. Do not catch at every layer; propagate and handle at boundary.

Async: Use try/catch in async functions; ensure promise rejections are handled (e.g. global handler or .catch) so they are logged and not unhandled. Register boundary handlers for `unhandledRejection`/`uncaughtException` (or the platform's equivalent): log with context, then exit for programmer errors — do not resume on corrupted state.

## Logging with Errors

- Level: error for failures that need attention; warn for recoverable (e.g. retry, fallback).
- Content: Log message, error type, stack (server-side only), request_id. Do not log secrets or full PII.
- Once: Log at the boundary where you handle the error; avoid logging the same error at every layer.
- Safe serialization: In log/error paths use a safe serializer (`safeStringify`, structured logger's own serializer), not raw `JSON.stringify` — it throws on circular references and `BigInt`, which can crash the very handler trying to report the failure.

## Rules

- Match existing project patterns (error classes, response shape, logging). If none exist, introduce a minimal consistent pattern and document it.
- Do not expose internal details (stack traces, DB errors, file paths) to end users; log them server-side only.
- Validation errors: return 400/422 with field-level details, not 500.

## Silent failures

The hardest class: it starts fine, returns 200, and quietly does nothing (or the wrong thing). Diagnose in order: unwired/unregistered component → undefined reference resolving to a no-op → type/signal mismatch dropping the payload → a swallowed error (`catch {}`, `|| true`) → sampling/filtering upstream making a downstream metric silently wrong. Never conclude a feature is absent because a key was rejected — look up that component's *own* key before shipping a degraded config, and disclose any tool/method substitution rather than reporting the original as missing.

## Checklist

- [ ] Inputs validated at boundary; validation errors returned in standard shape
- [ ] Errors mapped to correct HTTP status (no 200 for errors)
- [ ] User-facing message safe and actionable; detail in logs only
- [ ] Errors logged with context (request id, operation) before responding
- [ ] No empty catch; either handle, log and rethrow, or return result type

## Anti-patterns

| Anti-pattern | Better approach |
|--------------|-----------------|
| 200 OK with `{ success: false, error: "..." }` | Use proper HTTP status (4xx/5xx) and error body |
| Empty catch block | Log and rethrow, or return Err(result); document if intentionally ignoring |
| Discarded errors: `_ := f()` in Go, bare `except:` in Python, unchecked `Close()` | Handle the error, or document in place why ignoring is safe |
| `panic()` / `process.exit()` inside library code for operational errors | Return or throw a typed error; let the boundary decide |
| Logging full request/response on error | Log ids and summary; redact secrets and PII |
| Exposing stack trace to client | Log stack server-side; return generic message to client |
| Different error shape per endpoint | Use one envelope (code, message, details) across API |
| Transport-aware domain errors: services or repositories returning/throwing framework response objects or HTTP status codes | Throw transport-agnostic typed errors; exactly one global handler maps them to HTTP — business logic stays decoupled from delivery, easier to reuse and test |