Error handling patterns
29 free, verified agents, skills & packs for Claude Code - install with 'npx vanara install <name>'. Apache-2.0. From the Vanara catalog (206 items).
npx -y skills add vanara-agents/skills --skill error-handling-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 22 days oldThe repository was created 22 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
How to handle errors explicitly and consistently across an app — validate at boundaries, classify operational vs programmer errors, add context while propagating, retry transient failures with backoff, and never swallow. Covers JS/Python/Go/Rust patterns with runnable checks.
SKILL.md
7.5 KB, as published. Nobody here has run it
Error Handling Patterns
Errors are part of the contract, not an afterthought. A robust system handles errors where it can act,
propagates them where it can't, and never lets a failure vanish silently. This skill is the deep
reference: the decisions, the trade-offs, and the anti-patterns. Heavy detail lives in references/,
copy-paste material in examples/, and a runnable linter for swallowed errors in scripts/.
Mental model
Every error sits on two axes that decide what you do with it:
| Axis | Question | Consequence |
|---|---|---|
| Operational vs programmer | Is this an expected runtime condition (network down, bad input) or a bug (null deref, broken invariant)? | Operational → recover/retry/surface. Programmer → fail fast, let it crash, fix the code. |
| Recoverable vs fatal | Can the caller do something useful about it here? | Yes → handle locally. No → add context and propagate. |
Classifying wrong is the root cause of most bad error handling: retrying a programmer bug forever, or
crashing the process over a single bad HTTP request. The full taxonomy is in
references/error-taxonomy.md.
The five rules
- Validate at the boundary. Reject bad input early with a clear, specific message (fail fast). Never trust data crossing a trust boundary — HTTP bodies, env vars, file contents, API responses.
- Classify the error. Operational errors are values you handle; programmer errors are bugs you
surface loudly. Don't
catchaTypeErrorto "be safe". - Add context as you propagate. Wrap with what you were doing and preserve the original cause and
stack (
new Error(msg, { cause })in JS,fmt.Errorf("...: %w", err)in Go). - One owner for user-facing messaging. Translate to a friendly message at the edge (the HTTP layer, the UI). Keep full detail in server-side logs only — never leak stack traces to users.
- Never swallow. An empty
catch {}, a bareexcept: pass, or a.catch(() => {})is a defect. Runscripts/lint-empty-catch.mjsin CI to catch these automatically.
Adding context without losing the cause
The single most valuable habit: wrap errors with context while chaining the original, so the final log shows the full causal trail from the low-level failure up to the request that triggered it.
// Node 16.9+ supports the standard `cause` option on Error.
async function placeOrder(userId, cart) {
try {
const order = await db.orders.insert({ userId, items: cart.items });
return order;
} catch (err) {
// Wrap: keep the DB error as `cause`, add the business context.
throw new Error(`failed to place order for user ${userId}`, { cause: err });
}
}
# Python's `raise ... from` preserves the chain (shown as "The above exception
# was the direct cause of the following exception" in the traceback).
def place_order(user_id, cart):
try:
return db.orders.insert(user_id=user_id, items=cart.items)
except DBError as err:
raise OrderError(f"failed to place order for user {user_id}") from err
Language-by-language patterns (try/catch vs Result/Either vs panics) are in
references/language-patterns.md.
Retrying transient failures
Only operational, transient, idempotent failures are safe to retry — a 503 or a connection reset,
not a 400 or a NullPointerException. Retry with exponential backoff plus full jitter to avoid
synchronized retry storms (the "thundering herd"), and always cap attempts and total time.
async function withRetry(fn, { retries = 4, baseMs = 100, isRetryable } = {}) {
let attempt = 0;
for (;;) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt > retries || (isRetryable && !isRetryable(err))) throw err;
const backoff = baseMs * 2 ** (attempt - 1);
const jittered = Math.random() * backoff; // full jitter
await new Promise((r) => setTimeout(r, jittered));
}
}
}
When NOT to retry, circuit breakers, and budgets are covered in
references/retry-and-backoff.md.
Common pitfalls (anti-patterns)
- Swallowing errors — empty
catch {},except: pass,.catch(() => {}), orif err != nil {}in Go. The failure disappears and you debug blind. This is the #1 defect; lint for it. - Catch-log-rethrow-wrapped duplication — logging and rethrowing at every layer produces the same error logged ten times. Log once, at the boundary that owns the response.
- Catching too broadly —
except Exceptionorcatch (e)that also eatsKeyboardInterrupt, programmer bugs, orOutOfMemory. Catch the narrowest type you can actually handle. - Returning a sentinel instead of erroring — returning
null/-1/{}on failure forces every caller to remember the magic check; most won't. Prefer throwing or aResulttype. - Leaking internals to users — returning raw stack traces or SQL errors is both bad UX and a security leak (reveals schema, file paths, library versions).
- Retrying non-idempotent or non-transient operations — retrying a
POST /chargeon timeout can double-charge; retrying a400just wastes time. See the retry reference. - Losing the original cause —
throw new Error("save failed")without{ cause }discards the stack that tells you why.
When NOT to use / trade-offs
- Don't catch programmer errors to keep running. A bug that corrupted state should crash the process (let a supervisor like systemd/k8s/PM2 restart it clean) rather than limp along with bad data.
- Don't add a
Result/Eithertype to a small script — the ceremony outweighs the benefit. Plain exceptions are fine when there's one boundary and one owner. - Don't retry by default. Retries amplify load during incidents and can mask a real outage; only retry classified-transient, idempotent calls, and always with a budget.
- Defensive
try/catcheverywhere is a smell, not safety. It scatters handling, hides bugs, and makes control flow unreadable. Handle at meaningful boundaries.
Files in this package
references/error-taxonomy.md— operational vs programmer errors, and how the distinction drives recoveryreferences/retry-and-backoff.md— when to retry, exponential backoff, jitter, circuit breakers, budgetsreferences/language-patterns.md— try/catch vsResult/Eithervs panics across JS, Python, Go, Rustexamples/express-error-middleware.js— a complete centralized error handler with anAppErrorclassexamples/result-pattern.ts— a typedResult<T, E>with helpers, no exceptions for expected failuresscripts/lint-empty-catch.mjs— runnable Node linter that flags swallowed errors; supports--selftest
Pairs with the code-reviewer agent, the silent-failure-hunter agent, the rest-api-design skill
(for the HTTP error envelope), and the owasp-top10 skill (for not leaking sensitive data in errors).