agentleFS
ccpowerpackccpowerpack/claude-code-skills/skills/error-handling/SKILL.md

error-handling

Skill0 starsChanged 4 months agoMITView on GitHub

Design an error strategy for a module or service instead of sprinkling try/catch. Use when adding error handling, wrapping a flaky dependency, "make this robust", "handle failures here", reviewing code with catch blocks, or designing retries/timeouts. Refuses to write handlers until every failure source in the diff is classified into a taxonomy with one designated pattern per class.

80 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use convention-learner, caching and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • codewithmukeshSkill

    convention-learner

    Detects and enforces project-specific coding conventions by analyzing existing codebase patterns. Learns naming conventions, folder structure, test organization, and coding style from the existing code. Load when: "conventions", "coding standards", "project patterns", "enforce style", "detect patterns", "learn conventions", "code consistency".

    Also in 3 projects that use this

  • wpankSkill

    caching

    Caching strategies, invalidation, eviction policies, HTTP caching, distributed caching, and anti-patterns. Use when designing cache layers, choosing eviction policies, debugging stale data, or optimizing read-heavy workloads.

    Also in 6 projects that use this

  • diego-tobalinaSkill

    logging

    Logging best practices for Java (SLF4J/Logback) and Node.js (Winston/Pino). Use for structured logging, log levels, and avoiding sensitive data exposure.

    Also in 12 projects that use this

  • codewithmukeshSkill

    opentelemetry

    OpenTelemetry observability for .NET 10 applications. Covers traces, metrics, and logs using the OpenTelemetry SDK with OTLP export. Includes custom ActivitySource, IMeterFactory metrics, resource configuration, and Aspire Dashboard integration. Load this skill when setting up distributed tracing, custom metrics, OTLP export, or when the user mentions "OpenTelemetry", "OTLP", "traces", "spans", "Activity", "ActivitySource", "metrics", "IMeterFactory", "Meter", "Counter", "Histogram", "Gauge", "telemetry", "observability", "distributed tracing", "OTEL", or "Aspire Dashboard".

    Also in 3 projects that use this

  • MovGP0Skill

    modern-csharp

    Modern C# style guidance for Bendex and MicroSea code changes, including primary constructors, expression-bodied members, var usage, target-typed new, lambdas, and pattern matching.

    Also in 3 projects that use this

  • MicrosoftSkill

    resilience

    Apply resilience patterns (retry, backoff, circuit breaking) when writing or reviewing code that makes outbound HTTP/REST calls, database operations, or any I/O that can experience transient failures. Use this skill when: (1) writing new code that calls external services or APIs, (2) reviewing code that uses fetch, HTTP clients, or service-to-service calls, (3) adding queue consumers or workers that interact with remote systems, (4) debugging intermittent failures in production.

    Also in 4 projects that use this

The file

We split this skill into the rules it gives an agent

4 rules. 0 are common, 4 are distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 64 edited versions we read keep these.

  • 4

    Distinctive

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If two sources in the same class get different treatment, either the classification is wrong or you're inventing policy at the call site — …

    line 24
  2. Distinctive:

    Every surfaced error and every log line at the edge carries, checkably:

    line 97
  3. Distinctive:

    Before — one function, three failure modes, zero strategy:

    line 107
  4. Distinctive:

    After — one pattern per class, boundary translation at the top:

    line 123

“Common” is measured: kept by at least 6 in 10 of the 64 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Error Handling

The failure mode: error handling grows call-site by call-site — a try/catch here because a test flaked, a retry there because prod paged — until the module has five inconsistent policies and no strategy. Errors are a design surface, same as the API. The gate: you may not write a catch/rescue/match-on-Err block until every failure source in the diff is classified, and each class has exactly one designated handling pattern.

Phase 1 — Inventory and classify (gate)

Walk the diff (or module). List every operation that can fail: I/O, network calls, parsing, arithmetic on external data, lock acquisition, subprocess exits. Classify each into exactly one of four classes and write the table down before any handler code:

FAILURE INVENTORY — payment_worker.py
| Source                          | Class                | Policy (from taxonomy) |
| json.loads(webhook_body)        | invalid-input        | reject-with-identity   |
| stripe.Charge.create(...)       | retryable-transient  | retry-then-surface     |
| stripe: card_declined           | permanent-external   | surface, no retry      |
| db.save(receipt)                | retryable-transient  | retry-then-crash       |
| KeyError on our own config dict | bug                  | crash loudly           |

If two sources in the same class get different treatment, either the classification is wrong or you're inventing policy at the call site — both are gate failures.

Taxonomy — per-class policy

ClassDefinitionRetry?Surface to caller/user?Page someone?Process outcome
invalid-inputCaller/external party sent garbage; re-sending the same input fails identicallyNeverYes — with the identity of the bad input and what rule it brokeNo (it's their bug)Reject the request, keep serving
retryable-transientThe operation might succeed if repeated: timeouts, 429/503, lock contention, connection resetYes — backoff + jitter, bounded (cookbook below)Only after budget exhausted, as "temporarily unavailable, retryable"If exhaustion rate breaches thresholdContinue
permanent-externalDependency answered definitively no: 401 bad creds, card declined, quota hard-cap, 404 on required resourceNever (retrying a decline is how you get fined)Yes — translated to domain termsCreds/config failures: yes, that's opsContinue, or refuse startup if it's a boot dependency
bugOur invariant broke: impossible enum value, negative balance, missing "guaranteed" keyNeverGeneric 500/crash — never leak internalsYesCrash (or fail the unit of work) loudly and early — a bug handled "gracefully" runs on corrupted state

The classes are about what the correct reaction is, not where the error came from. The same Stripe SDK exception type may map to two classes (429 → transient, card_declined → permanent); classify by semantics, not by exception type alone.

Per-ecosystem idiom table

EcosystemIdiomUse when
GoWrap with context at each hop: fmt.Errorf("charging invoice %s: %w", inv.ID, err). Branch with errors.Is(err, ErrNotFound) / errors.As(err, &apiErr). Define sentinel errors / typed errors at package boundariesAlways. Naked return err from a non-trivial function loses the trail; err.Error() string matching is a firing offense
Rustthiserror for library/module error enums (callers match on variants); anyhow + .context("charging invoice inv_8XkQ") in application/binary codethiserror when callers must branch per-variant; anyhow when errors only travel up to be reported. The boundary between them IS your module boundary
PythonOne module-level hierarchy: class PaymentError(Exception) → CardDeclined, GatewayTimeout(PaymentError, RetryableError). Raise with raise CardDeclined(...) from e to keep the chainDesign the hierarchy in Phase 1 so callers can except PaymentError at the boundary instead of except Exception. Never except: bare; never swallow KeyboardInterrupt/SystemExit
TypeScriptDiscriminated-union results for expected domain outcomes: `type ChargeResult = {ok:true; receipt:Receipt}{ok:false; reason:'card_declined''insufficient_funds'}. throw` only for bug-class and infra failuresUnions force exhaustive switch handling at compile time — use them where "failure" is a normal business outcome. Exceptions for the truly exceptional; catch (e: unknown) and narrow, never catch (e: any)

Phase 2 — Boundary placement

Handlers live at boundaries; interior code propagates. Deciding where each class's designated pattern executes is half the design:

LayerRoleWhat it may do with errors
Edge (HTTP handler, queue consumer, CLI main, cron entry)The only place errors are finally handledTranslate class → response/exit code/nack; log ONCE with full context; decide retry-by-redelivery vs reject
Interior (services, domain logic)Propagate, adding context per hopWrap with identity (invoice inv_8XkQ); convert dependency errors into the module's own error types at the module boundary; never log-and-swallow
Adapter (DB client wrapper, HTTP client wrapper)ClassifyMap raw errors (OperationalError, status 429) into taxonomy classes; own the retry loop, because retry policy is a property of the dependency, not of every caller

Two rules fall out of this table. First, log at the edge, wrap in the interior — if every layer logs, one failure produces five log entries and the on-call engineer can't tell one incident from five. A catch block that logs and rethrows without adding new context is noise; either add the identity this layer knows (which invoice, which attempt, which shard) or don't touch the error. Second, retry in exactly one layer — the adapter that owns the idempotency mechanism — because stacked retry loops multiply (see cookbook).

Timeouts are part of placement: every network call gets an explicit deadline (no library default is trustworthy — many are infinite), and the timeout value must be shorter at inner layers than outer ones (inner 2s, edge 10s), otherwise the edge gives up while the interior is still retrying and the work is wasted or, worse, completes unobserved. A timeout is the canonical producer of the retryable-transient class — but remember it means "unknown outcome," not "didn't happen."

Retry cookbook (retryable-transient class only)

  • Formula: delay = min(cap, base * 2^attempt) * random(0.5, 1.0) — full-ish jitter. Typical: base=200ms, cap=30s, attempts=4. Without jitter, every client that failed together retries together (thundering herd).
  • Idempotency precondition — non-negotiable: retry only operations that are idempotent by nature (GET, PUT-with-full-body) or made so (POST with an Idempotency-Key, INSERT with a unique constraint + ON CONFLICT). A timeout does NOT mean the operation didn't happen; retrying a non-idempotent charge after a timeout is how customers get billed twice.
  • Retry budget: cap retries as a fraction of total traffic (e.g., retries ≤ 10% of requests, circuit-break past that). Per-request retries multiply across layers: client×3 → gateway×3 → service×3 = 27 attempts hitting a struggling database.
  • When retrying makes it worse: the dependency is overloaded (your retries are the load); the error is permanent-external misclassified as transient; you're inside a caller that also retries (pick ONE layer to own retries — usually the outermost that has the idempotency key).

Reference shape (the parameters above, in ~10 lines — adapt, don't import a framework for this):

def with_retry(op, retry_on, no_retry=(), attempts=4, base=0.2, cap=30.0):
    for i in range(attempts):
        try:
            return op()
        except no_retry:
            raise                                   # permanent-external: out immediately
        except retry_on as e:
            if i == attempts - 1:
                raise RetryExhausted(attempts=attempts) from e
            time.sleep(min(cap, base * 2**i) * random.uniform(0.5, 1.0))

Note what is deliberately absent: no retry on bare Exception (bug-class must escape on attempt one, not after 4 sleeps), and no logging inside the loop — the edge logs once, with attempts attached via the exception chain.

Edge translation table — the taxonomy class fixes the external representation; write it once per service and stop deciding per-endpoint:

ClassHTTP edgeQueue consumer edgeCLI edge
invalid-input400/422 + machine-readable code + field detailsReject to DLQ immediately (redelivery cannot fix garbage)Exit 2, usage-style message to stderr
retryable-transient (budget exhausted)503 + Retry-AfterNack for redelivery with delay; DLQ after N deliveriesExit 75 (EX_TEMPFAIL), message says "retry may succeed"
permanent-external4xx/502 mapped to domain code (card_declined → 402)DLQ with the classified reason attachedExit 1 with the dependency's verdict
bug500, generic body, full detail to logs/tracing onlyNack once (maybe it was corruption in transit), then DLQ + alertExit 70 (EX_SOFTWARE), stack trace to stderr

The queue column encodes the least-obvious rule: invalid-input must not be retried by redelivery — a poison message nacked in a loop burns the consumer forever. Classification is what tells the consumer to DLQ it on the first attempt.

Error message content checklist

Every surfaced error and every log line at the edge carries, checkably:

  • Identity of the failing input: the id, key, row number, or a safe prefix of the payload — never just the operation name
  • The expectation that broke: "expected ISO-8601 date, got '2026-13-01'" — rule + observed value
  • The taxonomy class's dispatch info: retryable or not; for user-facing errors, what the user can do about it
  • Correlation: request/trace id so the edge log line joins to the interior wrap chain
  • Nothing secret: no tokens, no full card numbers, no raw payload dumps of PII — identity means reference, not contents

Phase 3 — Worked example

Before — one function, three failure modes, zero strategy:

def process_webhook(body: bytes):
    try:
        event = json.loads(body)                     # invalid-input
        charge = stripe.Charge.create(               # transient OR permanent
            amount=event["amount"], idempotency_key=event["id"])
        db.save_receipt(charge)                      # transient
    except Exception as e:
        logger.error(f"webhook failed: {e}")         # which webhook? retriable?
        return {"ok": False}                         # caller can't tell garbage from outage

Every real problem is here: catch-log-continue means a DB blip silently drops a paid charge's receipt (money moved, no record — corrupted state); the caller gets the same {"ok": False} for a malformed body (never retry) and a Stripe timeout (must retry); the log line has no event id.

After — one pattern per class, boundary translation at the top:

def process_webhook(body: bytes) -> WebhookResult:
    try:
        event = parse_event(body)          # raises InvalidWebhook(payload_prefix=body[:64])
    except InvalidWebhook as e:
        return WebhookResult.reject(e)     # invalid-input: 400, no retry, identity attached

    charge = with_retry(                   # transient: backoff+jitter, budget=4
        lambda: stripe.Charge.create(amount=event.amount,
                                     idempotency_key=event.id),  # retry precondition
        retry_on=(RateLimitError, APIConnectionError),
        no_retry=(CardError,))             # permanent-external falls through
    # CardError propagates to the boundary handler → domain error, no retry, not paged

    with_retry(lambda: db.save_receipt(charge), retry_on=(OperationalError,),
               on_exhaust=raise_)          # transient→bug promotion: charge exists,
                                           # receipt doesn't → crash; the queue redelivers
                                           # and idempotency_key makes redelivery safe
    return WebhookResult.ok(event.id)

Note the last line's design move: "retries exhausted after money moved" is promoted to crash because continuing would run with corrupted state, and crash+redeliver is safe only because the idempotency key exists. That coupling is the strategy.

Output contract

Deliver: (1) the failure-inventory table, (2) the per-class pattern chosen (with ecosystem idiom named), (3) the retry parameters and their idempotency justification, (4) the code, (5) one sentence per catch block saying which class it handles. A catch block you can't assign to a class gets deleted.

Anti-patterns (hard no)

  • Catch-log-continue past a failure that leaves state half-written. Log-and-continue is only legal when the operation is provably complete-or-untouched.
  • Retrying any operation without stating why the retry is safe (idempotent by nature, or key/constraint shown).
  • catch (e) { log("operation failed"); rethrow e; } — log-and-rethrow without adding context (which invoice? which attempt?) guarantees the same error is logged N times with no new information. Add context or don't touch it.
  • Error messages/log lines without the failing input's identity. "Invalid date format" is useless; "invalid date '2026-13-01' in row 4823 of upload_7Kx.csv" is actionable.
  • One generic AppError/CustomException for everything — callers are forced to string-match messages, which is the same as no error types.
  • Designing handlers before the inventory table exists.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.