vanara 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +140 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +168 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +112 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +156 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +185 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +168 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +163 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +177 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +163 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +146 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-handling-patterns
|
|
3
|
+
description: 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.
|
|
4
|
+
type: skill
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
updated: 2026-06-29
|
|
7
|
+
---
|
|
8
|
+
# Error Handling Patterns
|
|
9
|
+
|
|
10
|
+
Errors are part of the contract, not an afterthought. A robust system **handles errors where it can act,
|
|
11
|
+
propagates them where it can't, and never lets a failure vanish silently**. This skill is the deep
|
|
12
|
+
reference: the decisions, the trade-offs, and the anti-patterns. Heavy detail lives in `references/`,
|
|
13
|
+
copy-paste material in `examples/`, and a runnable linter for swallowed errors in `scripts/`.
|
|
14
|
+
|
|
15
|
+
## Mental model
|
|
16
|
+
|
|
17
|
+
Every error sits on two axes that decide what you do with it:
|
|
18
|
+
|
|
19
|
+
| Axis | Question | Consequence |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| **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. |
|
|
22
|
+
| **Recoverable vs fatal** | Can the caller do something useful about it here? | Yes → handle locally. No → add context and propagate. |
|
|
23
|
+
|
|
24
|
+
Classifying wrong is the root cause of most bad error handling: retrying a programmer bug forever, or
|
|
25
|
+
crashing the process over a single bad HTTP request. The full taxonomy is in
|
|
26
|
+
[`references/error-taxonomy.md`](references/error-taxonomy.md).
|
|
27
|
+
|
|
28
|
+
## The five rules
|
|
29
|
+
|
|
30
|
+
1. **Validate at the boundary.** Reject bad input early with a clear, specific message (fail fast). Never
|
|
31
|
+
trust data crossing a trust boundary — HTTP bodies, env vars, file contents, API responses.
|
|
32
|
+
2. **Classify the error.** Operational errors are values you handle; programmer errors are bugs you
|
|
33
|
+
surface loudly. Don't `catch` a `TypeError` to "be safe".
|
|
34
|
+
3. **Add context as you propagate.** Wrap with *what you were doing* and preserve the original cause and
|
|
35
|
+
stack (`new Error(msg, { cause })` in JS, `fmt.Errorf("...: %w", err)` in Go).
|
|
36
|
+
4. **One owner for user-facing messaging.** Translate to a friendly message at the edge (the HTTP layer,
|
|
37
|
+
the UI). Keep full detail in server-side logs only — never leak stack traces to users.
|
|
38
|
+
5. **Never swallow.** An empty `catch {}`, a bare `except: pass`, or a `.catch(() => {})` is a defect.
|
|
39
|
+
Run `scripts/lint-empty-catch.mjs` in CI to catch these automatically.
|
|
40
|
+
|
|
41
|
+
## Adding context without losing the cause
|
|
42
|
+
|
|
43
|
+
The single most valuable habit: wrap errors with context while chaining the original, so the final log
|
|
44
|
+
shows the full causal trail from the low-level failure up to the request that triggered it.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// Node 16.9+ supports the standard `cause` option on Error.
|
|
48
|
+
async function placeOrder(userId, cart) {
|
|
49
|
+
try {
|
|
50
|
+
const order = await db.orders.insert({ userId, items: cart.items });
|
|
51
|
+
return order;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Wrap: keep the DB error as `cause`, add the business context.
|
|
54
|
+
throw new Error(`failed to place order for user ${userId}`, { cause: err });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# Python's `raise ... from` preserves the chain (shown as "The above exception
|
|
61
|
+
# was the direct cause of the following exception" in the traceback).
|
|
62
|
+
def place_order(user_id, cart):
|
|
63
|
+
try:
|
|
64
|
+
return db.orders.insert(user_id=user_id, items=cart.items)
|
|
65
|
+
except DBError as err:
|
|
66
|
+
raise OrderError(f"failed to place order for user {user_id}") from err
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Language-by-language patterns (try/catch vs `Result`/`Either` vs panics) are in
|
|
70
|
+
[`references/language-patterns.md`](references/language-patterns.md).
|
|
71
|
+
|
|
72
|
+
## Retrying transient failures
|
|
73
|
+
|
|
74
|
+
Only **operational, transient, idempotent** failures are safe to retry — a 503 or a connection reset,
|
|
75
|
+
not a 400 or a `NullPointerException`. Retry with **exponential backoff plus full jitter** to avoid
|
|
76
|
+
synchronized retry storms (the "thundering herd"), and always cap attempts and total time.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
async function withRetry(fn, { retries = 4, baseMs = 100, isRetryable } = {}) {
|
|
80
|
+
let attempt = 0;
|
|
81
|
+
for (;;) {
|
|
82
|
+
try {
|
|
83
|
+
return await fn();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
attempt++;
|
|
86
|
+
if (attempt > retries || (isRetryable && !isRetryable(err))) throw err;
|
|
87
|
+
const backoff = baseMs * 2 ** (attempt - 1);
|
|
88
|
+
const jittered = Math.random() * backoff; // full jitter
|
|
89
|
+
await new Promise((r) => setTimeout(r, jittered));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When NOT to retry, circuit breakers, and budgets are covered in
|
|
96
|
+
[`references/retry-and-backoff.md`](references/retry-and-backoff.md).
|
|
97
|
+
|
|
98
|
+
## Common pitfalls (anti-patterns)
|
|
99
|
+
|
|
100
|
+
- **Swallowing errors** — empty `catch {}`, `except: pass`, `.catch(() => {})`, or `if err != nil {}` in
|
|
101
|
+
Go. The failure disappears and you debug blind. This is the #1 defect; lint for it.
|
|
102
|
+
- **Catch-log-rethrow-wrapped duplication** — logging *and* rethrowing at every layer produces the same
|
|
103
|
+
error logged ten times. Log once, at the boundary that owns the response.
|
|
104
|
+
- **Catching too broadly** — `except Exception` or `catch (e)` that also eats `KeyboardInterrupt`,
|
|
105
|
+
programmer bugs, or `OutOfMemory`. Catch the narrowest type you can actually handle.
|
|
106
|
+
- **Returning a sentinel instead of erroring** — returning `null`/`-1`/`{}` on failure forces every
|
|
107
|
+
caller to remember the magic check; most won't. Prefer throwing or a `Result` type.
|
|
108
|
+
- **Leaking internals to users** — returning raw stack traces or SQL errors is both bad UX and a security
|
|
109
|
+
leak (reveals schema, file paths, library versions).
|
|
110
|
+
- **Retrying non-idempotent or non-transient operations** — retrying a `POST /charge` on timeout can
|
|
111
|
+
double-charge; retrying a `400` just wastes time. See the retry reference.
|
|
112
|
+
- **Losing the original cause** — `throw new Error("save failed")` without `{ cause }` discards the stack
|
|
113
|
+
that tells you *why*.
|
|
114
|
+
|
|
115
|
+
## When NOT to use / trade-offs
|
|
116
|
+
|
|
117
|
+
- **Don't catch programmer errors to keep running.** A bug that corrupted state should crash the process
|
|
118
|
+
(let a supervisor like systemd/k8s/PM2 restart it clean) rather than limp along with bad data.
|
|
119
|
+
- **Don't add a `Result`/`Either` type to a small script** — the ceremony outweighs the benefit. Plain
|
|
120
|
+
exceptions are fine when there's one boundary and one owner.
|
|
121
|
+
- **Don't retry by default.** Retries amplify load during incidents and can mask a real outage; only
|
|
122
|
+
retry classified-transient, idempotent calls, and always with a budget.
|
|
123
|
+
- **Defensive `try/catch` everywhere is a smell**, not safety. It scatters handling, hides bugs, and
|
|
124
|
+
makes control flow unreadable. Handle at meaningful boundaries.
|
|
125
|
+
|
|
126
|
+
## Files in this package
|
|
127
|
+
|
|
128
|
+
- [`references/error-taxonomy.md`](references/error-taxonomy.md) — operational vs programmer errors, and how the distinction drives recovery
|
|
129
|
+
- [`references/retry-and-backoff.md`](references/retry-and-backoff.md) — when to retry, exponential backoff, jitter, circuit breakers, budgets
|
|
130
|
+
- [`references/language-patterns.md`](references/language-patterns.md) — try/catch vs `Result`/`Either` vs panics across JS, Python, Go, Rust
|
|
131
|
+
- [`examples/express-error-middleware.js`](examples/express-error-middleware.js) — a complete centralized error handler with an `AppError` class
|
|
132
|
+
- [`examples/result-pattern.ts`](examples/result-pattern.ts) — a typed `Result<T, E>` with helpers, no exceptions for expected failures
|
|
133
|
+
- [`scripts/lint-empty-catch.mjs`](scripts/lint-empty-catch.mjs) — runnable Node linter that flags swallowed errors; supports `--selftest`
|
|
134
|
+
|
|
135
|
+
Pairs with the `code-reviewer` agent, the `silent-failure-hunter` agent, the `rest-api-design` skill
|
|
136
|
+
(for the HTTP error envelope), and the `owasp-top10` skill (for not leaking sensitive data in errors).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Centralized error handling for an Express app.
|
|
2
|
+
//
|
|
3
|
+
// Pattern: routes never format error responses themselves. They throw (or pass
|
|
4
|
+
// to next()) typed AppErrors, and ONE error-handling middleware translates them
|
|
5
|
+
// to a consistent HTTP envelope, logs full detail server-side, and hides
|
|
6
|
+
// internals from the client.
|
|
7
|
+
//
|
|
8
|
+
// Run a quick demo: node express-error-middleware.js (no deps; uses a fake req/res)
|
|
9
|
+
|
|
10
|
+
// --- 1. A typed, operational error -----------------------------------------
|
|
11
|
+
|
|
12
|
+
class AppError extends Error {
|
|
13
|
+
constructor(message, { code, status = 500, retryable = false, cause } = {}) {
|
|
14
|
+
super(message, { cause });
|
|
15
|
+
this.name = 'AppError';
|
|
16
|
+
this.code = code; // stable, machine-readable: 'order_not_found'
|
|
17
|
+
this.status = status; // maps to HTTP status
|
|
18
|
+
this.retryable = retryable;
|
|
19
|
+
this.isOperational = true; // distinguishes from programmer bugs
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Convenience constructors for common cases.
|
|
24
|
+
const NotFound = (msg, code = 'not_found') => new AppError(msg, { code, status: 404 });
|
|
25
|
+
const BadRequest = (msg, code = 'bad_request') => new AppError(msg, { code, status: 400 });
|
|
26
|
+
const Conflict = (msg, code = 'conflict') => new AppError(msg, { code, status: 409 });
|
|
27
|
+
|
|
28
|
+
// --- 2. Wrap async route handlers so rejections reach the error middleware --
|
|
29
|
+
// Without this, a thrown error in an async handler becomes an unhandledRejection
|
|
30
|
+
// instead of hitting Express's error pipeline.
|
|
31
|
+
const asyncHandler = (fn) => (req, res, next) =>
|
|
32
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
33
|
+
|
|
34
|
+
// --- 3. The ONE error-handling middleware (must have 4 args) ----------------
|
|
35
|
+
|
|
36
|
+
function errorMiddleware(logger) {
|
|
37
|
+
return (err, req, res, _next) => {
|
|
38
|
+
const operational = err instanceof AppError && err.isOperational;
|
|
39
|
+
const status = operational ? err.status : 500;
|
|
40
|
+
|
|
41
|
+
// Log full detail server-side ALWAYS — including the cause chain.
|
|
42
|
+
logger.error('request failed', {
|
|
43
|
+
method: req.method,
|
|
44
|
+
path: req.path,
|
|
45
|
+
status,
|
|
46
|
+
code: err.code,
|
|
47
|
+
message: err.message,
|
|
48
|
+
cause: err.cause && String(err.cause),
|
|
49
|
+
stack: err.stack,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Never leak internals to the client on a non-operational (bug) error.
|
|
53
|
+
const body = operational
|
|
54
|
+
? { data: null, error: { code: err.code, message: err.message } }
|
|
55
|
+
: { data: null, error: { code: 'internal_error', message: 'Something went wrong.' } };
|
|
56
|
+
|
|
57
|
+
res.status(status).json(body);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- 4. Example routes ------------------------------------------------------
|
|
62
|
+
// app.get('/orders/:id', asyncHandler(async (req, res) => {
|
|
63
|
+
// const order = await db.orders.find(req.params.id);
|
|
64
|
+
// if (!order) throw NotFound(`order ${req.params.id} not found`, 'order_not_found');
|
|
65
|
+
// res.json({ data: order, error: null });
|
|
66
|
+
// }));
|
|
67
|
+
// app.use(errorMiddleware(logger)); // registered LAST, after all routes.
|
|
68
|
+
|
|
69
|
+
// --- Tiny self-contained demo (no Express needed) --------------------------
|
|
70
|
+
|
|
71
|
+
function demo() {
|
|
72
|
+
const logger = { error: (m, ctx) => console.log(`[log] ${m}`, ctx.code, '|', ctx.message) };
|
|
73
|
+
const handle = errorMiddleware(logger);
|
|
74
|
+
const fakeReq = { method: 'GET', path: '/orders/42' };
|
|
75
|
+
const fakeRes = {
|
|
76
|
+
status(s) { this._s = s; return this; },
|
|
77
|
+
json(b) { console.log(`-> HTTP ${this._s}`, JSON.stringify(b)); },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
console.log('Operational error (safe to show user):');
|
|
81
|
+
handle(NotFound('order 42 not found', 'order_not_found'), fakeReq, fakeRes);
|
|
82
|
+
|
|
83
|
+
console.log('\nProgrammer bug (internals hidden from user):');
|
|
84
|
+
handle(new TypeError("Cannot read properties of undefined (reading 'id')"), fakeReq, fakeRes);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { AppError, NotFound, BadRequest, Conflict, asyncHandler, errorMiddleware };
|
|
88
|
+
|
|
89
|
+
// Run the demo when executed directly (robust on Windows paths with spaces).
|
|
90
|
+
import { fileURLToPath } from 'node:url';
|
|
91
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
92
|
+
demo();
|
|
93
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// A typed Result<T, E> for TypeScript: model *expected* failures as values so the
|
|
2
|
+
// compiler forces callers to handle them, instead of throwing for control flow.
|
|
3
|
+
//
|
|
4
|
+
// Reserve `throw` for programmer errors / truly exceptional paths. Use Result for
|
|
5
|
+
// validation, "not found", parse failures, and other ordinary outcomes.
|
|
6
|
+
|
|
7
|
+
// --- The type --------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export type Ok<T> = { readonly ok: true; readonly value: T };
|
|
10
|
+
export type Err<E> = { readonly ok: false; readonly error: E };
|
|
11
|
+
export type Result<T, E> = Ok<T> | Err<E>;
|
|
12
|
+
|
|
13
|
+
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
|
|
14
|
+
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
|
|
15
|
+
|
|
16
|
+
// --- Helpers ---------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Transform the success value, leaving errors untouched. */
|
|
19
|
+
export function map<T, U, E>(r: Result<T, E>, fn: (t: T) => U): Result<U, E> {
|
|
20
|
+
return r.ok ? ok(fn(r.value)) : r;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Chain operations that can themselves fail (railway-oriented). */
|
|
24
|
+
export function flatMap<T, U, E>(
|
|
25
|
+
r: Result<T, E>,
|
|
26
|
+
fn: (t: T) => Result<U, E>,
|
|
27
|
+
): Result<U, E> {
|
|
28
|
+
return r.ok ? fn(r.value) : r;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Provide a fallback for the error case. */
|
|
32
|
+
export function unwrapOr<T, E>(r: Result<T, E>, fallback: T): T {
|
|
33
|
+
return r.ok ? r.value : fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wrap a throwing function so its exception becomes an Err value. */
|
|
37
|
+
export function tryCatch<T>(fn: () => T): Result<T, Error> {
|
|
38
|
+
try {
|
|
39
|
+
return ok(fn());
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return err(e instanceof Error ? e : new Error(String(e)));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Domain error type -----------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export type ValidationError = { code: 'validation'; field: string; message: string };
|
|
48
|
+
|
|
49
|
+
// --- Example: validation that returns Result instead of throwing -----------
|
|
50
|
+
|
|
51
|
+
interface SignupInput {
|
|
52
|
+
email: string;
|
|
53
|
+
age: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateSignup(input: SignupInput): Result<SignupInput, ValidationError> {
|
|
57
|
+
if (!input.email.includes('@')) {
|
|
58
|
+
return err({ code: 'validation', field: 'email', message: 'must be a valid email' });
|
|
59
|
+
}
|
|
60
|
+
if (input.age < 18) {
|
|
61
|
+
return err({ code: 'validation', field: 'age', message: 'must be 18 or older' });
|
|
62
|
+
}
|
|
63
|
+
return ok(input);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Usage: the caller CANNOT forget the error case (compiler enforces it) --
|
|
67
|
+
|
|
68
|
+
export function handleSignup(input: SignupInput): string {
|
|
69
|
+
const result = validateSignup(input);
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
// result is narrowed to Err<ValidationError> here.
|
|
72
|
+
return `rejected: ${result.error.field} ${result.error.message}`;
|
|
73
|
+
}
|
|
74
|
+
// result is narrowed to Ok<SignupInput> here.
|
|
75
|
+
return `accepted: ${result.value.email}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Example with chaining:
|
|
79
|
+
// const parsed = tryCatch(() => JSON.parse(raw));
|
|
80
|
+
// const validated = flatMap(parsed, (obj) => validateSignup(obj as SignupInput));
|
|
81
|
+
// const msg = validated.ok ? 'ok' : `bad: ${validated.error.message}`;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Error Taxonomy — Operational vs Programmer Errors
|
|
2
|
+
|
|
3
|
+
The most important classification in error handling. Get it wrong and you either crash on recoverable
|
|
4
|
+
conditions or limp along with corrupted state.
|
|
5
|
+
|
|
6
|
+
## The two categories
|
|
7
|
+
|
|
8
|
+
### Operational errors
|
|
9
|
+
Expected runtime conditions a correct program will still encounter. They are **values to be handled**,
|
|
10
|
+
not bugs.
|
|
11
|
+
|
|
12
|
+
- Failed to connect to a server / connection reset
|
|
13
|
+
- Request timed out
|
|
14
|
+
- Server returned `503` / `429`
|
|
15
|
+
- Invalid user input (failed validation)
|
|
16
|
+
- File not found, disk full, permission denied
|
|
17
|
+
- Out-of-money / out-of-stock business conditions
|
|
18
|
+
|
|
19
|
+
**Response:** recover, retry (if transient + idempotent), or surface a clear message to the caller/user.
|
|
20
|
+
These should *never* crash the process.
|
|
21
|
+
|
|
22
|
+
### Programmer errors
|
|
23
|
+
Bugs. The code is wrong. The program is now in a state its author did not anticipate.
|
|
24
|
+
|
|
25
|
+
- `undefined is not a function` / `TypeError` / `AttributeError`
|
|
26
|
+
- Reading a property of `null`
|
|
27
|
+
- Passing the wrong type to a function
|
|
28
|
+
- Forgetting to `await` a promise
|
|
29
|
+
- A violated invariant / failed assertion
|
|
30
|
+
|
|
31
|
+
**Response:** **fail fast and loud.** Do not `catch` them to "be safe". Let the process crash and let a
|
|
32
|
+
supervisor (systemd, Kubernetes, PM2, a worker pool) restart it in a known-good state. Catching a bug to
|
|
33
|
+
continue running risks acting on corrupted data.
|
|
34
|
+
|
|
35
|
+
## Why the distinction drives everything
|
|
36
|
+
|
|
37
|
+
| Decision | Operational | Programmer |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Catch it? | Yes, where you can act | No — let it propagate to a top-level crash |
|
|
40
|
+
| Retry it? | If transient + idempotent | Never (the bug is deterministic) |
|
|
41
|
+
| Show the user? | A friendly, specific message | A generic "something went wrong" + 500, log internally |
|
|
42
|
+
| Alert on it? | Rate/aggregate (it's expected sometimes) | Page someone — it's a bug |
|
|
43
|
+
|
|
44
|
+
## Modeling operational errors as types
|
|
45
|
+
|
|
46
|
+
Give operational errors a stable, machine-readable identity so callers and the HTTP layer can branch on
|
|
47
|
+
them without string-matching messages.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
class AppError extends Error {
|
|
51
|
+
constructor(message, { code, status = 500, retryable = false, cause } = {}) {
|
|
52
|
+
super(message, { cause });
|
|
53
|
+
this.name = 'AppError';
|
|
54
|
+
this.code = code; // e.g. 'order_not_found' — stable, machine-readable
|
|
55
|
+
this.status = status; // maps to an HTTP status at the boundary
|
|
56
|
+
this.retryable = retryable;
|
|
57
|
+
this.isOperational = true; // distinguishes from programmer bugs
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// At the top-level handler:
|
|
62
|
+
function isOperational(err) {
|
|
63
|
+
return err instanceof AppError && err.isOperational === true;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
At your crash handler, **only** swallow-and-respond for `isOperational` errors; rethrow everything else
|
|
68
|
+
so the process restarts.
|
|
69
|
+
|
|
70
|
+
## The uncaught handler
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
process.on('uncaughtException', (err) => {
|
|
74
|
+
logger.fatal('uncaught exception — crashing', { err });
|
|
75
|
+
// Do NOT resume. Flush logs, then exit; let the supervisor restart.
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
78
|
+
process.on('unhandledRejection', (reason) => {
|
|
79
|
+
// Promote to an exception so the above handler runs.
|
|
80
|
+
throw reason;
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Pitfalls
|
|
85
|
+
- **Treating all errors the same.** A blanket `try/catch` around `main()` that logs and continues turns
|
|
86
|
+
bugs into silent data corruption.
|
|
87
|
+
- **Retrying programmer errors.** They're deterministic — the retry fails identically while burning time
|
|
88
|
+
and load.
|
|
89
|
+
- **A "500 for everything" API.** Validation failures are operational and should be `400/422`, not `500`.
|
|
90
|
+
- **Catching to add a log then re-throwing at every layer** — you get the same bug logged N times with no
|
|
91
|
+
added signal. Decide one owner.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Language Patterns — Exceptions vs Result vs Panics
|
|
2
|
+
|
|
3
|
+
Different languages encode failure differently. Using the idiom the language was designed for keeps code
|
|
4
|
+
readable and tooling effective.
|
|
5
|
+
|
|
6
|
+
## JavaScript / TypeScript — exceptions + `cause`
|
|
7
|
+
|
|
8
|
+
Throw/`try`/`catch` is idiomatic. Since Node 16.9 / ES2022, always chain the cause:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
try {
|
|
12
|
+
await writeFile(path, data);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
throw new Error(`failed to persist ${path}`, { cause: err });
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For *expected* failures (validation, "not found") in TypeScript, a `Result` type makes the failure path
|
|
19
|
+
explicit in the signature and forces the caller to handle it — see [`result-pattern.ts`](../examples/result-pattern.ts).
|
|
20
|
+
Reserve `throw` for exceptional/programmer paths.
|
|
21
|
+
|
|
22
|
+
Async caveat: a rejected promise that isn't `await`ed or `.catch`ed becomes an `unhandledRejection`.
|
|
23
|
+
Never write `.catch(() => {})` to silence it — that's swallowing.
|
|
24
|
+
|
|
25
|
+
## Python — exceptions + `raise from`
|
|
26
|
+
|
|
27
|
+
Exceptions are the idiom. Preserve the chain and catch narrowly:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
try:
|
|
31
|
+
config = json.loads(raw)
|
|
32
|
+
except json.JSONDecodeError as err:
|
|
33
|
+
raise ConfigError("config file is not valid JSON") from err
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Catch the **narrowest** exception (`except KeyError`, not bare `except:`).
|
|
37
|
+
- A bare `except:` even catches `KeyboardInterrupt`/`SystemExit` — almost always a bug.
|
|
38
|
+
- Use `else` for the success path and `finally` for cleanup; or a context manager (`with`) for
|
|
39
|
+
acquire/release so cleanup can't be forgotten.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# Anti-pattern this skill's linter flags:
|
|
43
|
+
try:
|
|
44
|
+
do_thing()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass # swallowed — the failure is now invisible
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Go — errors as values
|
|
50
|
+
|
|
51
|
+
Go has no exceptions for ordinary failures; errors are returned values you must check. Wrap with `%w` to
|
|
52
|
+
preserve the chain, then unwrap with `errors.Is`/`errors.As`.
|
|
53
|
+
|
|
54
|
+
```go
|
|
55
|
+
func loadUser(id string) (*User, error) {
|
|
56
|
+
u, err := db.Get(id)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return nil, fmt.Errorf("loadUser %s: %w", id, err)
|
|
59
|
+
}
|
|
60
|
+
return u, nil
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// caller:
|
|
64
|
+
if err != nil {
|
|
65
|
+
if errors.Is(err, sql.ErrNoRows) { /* operational: 404 */ }
|
|
66
|
+
return err // propagate with context already attached
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `panic` is for **programmer errors / unrecoverable** states only — not control flow.
|
|
71
|
+
- `if err != nil {}` with an empty body is the Go form of swallowing.
|
|
72
|
+
- `recover()` belongs at goroutine/server boundaries to stop one bad request from crashing the process,
|
|
73
|
+
not as a general try/catch.
|
|
74
|
+
|
|
75
|
+
## Rust — `Result<T, E>` and `?`
|
|
76
|
+
|
|
77
|
+
Failure is in the type system. `Result<T, E>` must be handled (the compiler warns on an unused one), and
|
|
78
|
+
`?` propagates concisely while converting error types via `From`.
|
|
79
|
+
|
|
80
|
+
```rust
|
|
81
|
+
fn read_config(path: &str) -> Result<Config, ConfigError> {
|
|
82
|
+
let raw = std::fs::read_to_string(path)?; // io::Error -> ConfigError via From
|
|
83
|
+
let cfg: Config = toml::from_str(&raw)?; // toml error -> ConfigError via From
|
|
84
|
+
Ok(cfg)
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `Option<T>` models absence; `Result<T, E>` models failure with a reason.
|
|
89
|
+
- `panic!`, `.unwrap()`, `.expect()` are for unrecoverable/programmer errors — avoid in library code.
|
|
90
|
+
- Crates like `thiserror` (typed library errors) and `anyhow` (application-level context) are the
|
|
91
|
+
ecosystem standard.
|
|
92
|
+
|
|
93
|
+
## Cross-cutting principles
|
|
94
|
+
|
|
95
|
+
| Principle | JS/TS | Python | Go | Rust |
|
|
96
|
+
|---|---|---|---|---|
|
|
97
|
+
| Preserve cause | `{ cause }` | `raise ... from` | `%w` + `errors.Is/As` | `source()` / `?` + `From` |
|
|
98
|
+
| Expected-failure idiom | `Result` type | exceptions | error return | `Result<T,E>` |
|
|
99
|
+
| Programmer-error idiom | `throw` (crash) | exception (crash) | `panic` | `panic!`/`unwrap` |
|
|
100
|
+
| Swallow anti-pattern | `.catch(()=>{})` | `except: pass` | `if err != nil {}` | `let _ = ...;` ignoring `Result` |
|
|
101
|
+
|
|
102
|
+
The throughline: **make failure explicit, preserve the cause, handle at a boundary, and never silence.**
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Retry & Backoff
|
|
2
|
+
|
|
3
|
+
Retries turn transient failures into success — or turn a small blip into a self-inflicted outage. The
|
|
4
|
+
difference is discipline: retry only the right errors, back off exponentially, add jitter, and cap the
|
|
5
|
+
budget.
|
|
6
|
+
|
|
7
|
+
## What is safe to retry
|
|
8
|
+
|
|
9
|
+
A call is retry-safe only if **all three** hold:
|
|
10
|
+
|
|
11
|
+
1. **Operational + transient** — `503`, `429`, connection reset, timeout. Not `400`, `401`, `404`, `422`,
|
|
12
|
+
and never a programmer bug.
|
|
13
|
+
2. **Idempotent** — the same call twice has the same effect. `GET`, `PUT`, `DELETE` usually qualify.
|
|
14
|
+
`POST` does *not* unless protected by an idempotency key.
|
|
15
|
+
3. **Within budget** — you have attempts and time left.
|
|
16
|
+
|
|
17
|
+
Retrying a non-idempotent `POST /charge` after a timeout can double-charge the customer (the first
|
|
18
|
+
request may have succeeded; only the *response* was lost). Use an `Idempotency-Key` so the server
|
|
19
|
+
deduplicates — see the `rest-api-design` skill.
|
|
20
|
+
|
|
21
|
+
## Exponential backoff with jitter
|
|
22
|
+
|
|
23
|
+
Fixed-interval retries from many clients synchronize into waves that hammer a recovering service (the
|
|
24
|
+
**thundering herd**). Exponential backoff spreads attempts out; **jitter** desynchronizes clients.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// Full jitter (AWS-recommended): sleep = random(0, base * 2**attempt), capped.
|
|
28
|
+
function backoffDelay(attempt, { baseMs = 100, capMs = 10_000 } = {}) {
|
|
29
|
+
const exp = Math.min(capMs, baseMs * 2 ** attempt);
|
|
30
|
+
return Math.random() * exp;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Strategy | Behavior | Verdict |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Fixed delay | every client retries at the same cadence | thundering herd |
|
|
37
|
+
| Exponential, no jitter | spreads over time but clients still align | better, still synchronized |
|
|
38
|
+
| Exponential + **full jitter** | random spread over the window | **recommended default** |
|
|
39
|
+
|
|
40
|
+
## Caps and budgets
|
|
41
|
+
|
|
42
|
+
- **Max attempts:** typically 3–5. Beyond that you're usually masking a real outage.
|
|
43
|
+
- **Total deadline:** cap wall-clock time (e.g. 30s) so a request doesn't hang the caller.
|
|
44
|
+
- **Respect `Retry-After`:** if the server sends it (on `429`/`503`), honor it over your own backoff.
|
|
45
|
+
- **Retry budget:** limit retries to a small fraction (e.g. 10%) of total requests so retries can't more
|
|
46
|
+
than ~double load during an incident.
|
|
47
|
+
|
|
48
|
+
## Circuit breakers
|
|
49
|
+
|
|
50
|
+
When a dependency is clearly down, stop sending it traffic — retries just pile on. A circuit breaker
|
|
51
|
+
tracks the recent failure rate and **opens** (fails fast) past a threshold, periodically letting a
|
|
52
|
+
**half-open** trial request through to test recovery.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
CLOSED --(failures > threshold)--> OPEN --(cooldown elapsed)--> HALF-OPEN
|
|
56
|
+
^ |
|
|
57
|
+
+--------------------(trial request succeeds)------------------------+
|
|
58
|
+
(trial fails -> back to OPEN)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This protects the failing service (lets it recover) and the caller (fails fast instead of waiting on
|
|
62
|
+
timeouts).
|
|
63
|
+
|
|
64
|
+
## Timeouts come first
|
|
65
|
+
|
|
66
|
+
Retries are pointless without timeouts — a hung connection with no timeout never fails, so it never
|
|
67
|
+
retries. Always set a per-attempt timeout *and* an overall deadline. `AbortController` (JS),
|
|
68
|
+
`context.WithTimeout` (Go), and `asyncio.wait_for` (Python) are the standard tools.
|
|
69
|
+
|
|
70
|
+
## Pitfalls
|
|
71
|
+
- **Retrying everything** — retrying `400`/`401`/`422` is pure waste; they'll fail identically.
|
|
72
|
+
- **No jitter** — synchronized retries DDoS your own recovering service.
|
|
73
|
+
- **Unbounded retries** — a stuck dependency turns into an infinite loop and resource exhaustion.
|
|
74
|
+
- **Retrying non-idempotent writes** without an idempotency key — duplicate charges, duplicate orders.
|
|
75
|
+
- **Nested retries** — retry at the HTTP client *and* the service *and* the caller multiplies attempts
|
|
76
|
+
exponentially (3×3×3 = 27). Retry at exactly one layer.
|