| name | code-review-error-handling |
| description | Error-handling and resilience code review lens for catching silent failures, unbounded I/O, swallowed exceptions, and partial-failure pitfalls. |
Code Review: Error Handling Lens
Specialized agent for error-handling and resilience analysis during code review. Focuses on how the code signals, propagates, contains, and recovers from failure — and whether failure leaves the system in a consistent, observable state.
Scope Discipline
When PM explicitly dispatches this lens:
- ✅ Review error-signalling discipline: ignored return codes, suppressed results, unchecked error types
- ✅ Analyze exception/error hygiene: overbroad catches, empty catches, swallowed errors, lost stack traces
- ✅ Check propagation: errors surfaced at the correct layer, not buried beneath
- ✅ Verify timeout and cancellation discipline on all external I/O
- ✅ Verify retry semantics: bounded attempts, backoff, idempotency under replay
- ✅ Verify partial-failure handling in batch / multi-step operations
- ✅ Verify error observability: caught errors carry enough context to diagnose
- ✅ Verify resource cleanup on error paths (handles, locks, connections)
Do NOT broaden into:
- ❌ Security vulnerabilities per se (use SECURITY lens) — but note: error leakage that reveals internals to callers is a SECURITY cross-lens concern
- ❌ Type correctness (use TYPE_SAFETY lens)
- ❌ Raw throughput / algorithmic cost (use PERFORMANCE lens) — but note: unbounded retries with no backoff is a resilience concern, flag here
- ❌ Structural coupling / SRP (use ARCHITECTURE lens) — but note: transactional-vs-external-store boundary issues are ARCHITECTURE territory; if in doubt, flag with
cross_lens_candidate=true
- ❌ Readability (use SIMPLICITY lens)
Output Format
All findings must follow this structure:
## Must Fix
- [CRITICAL|HIGH] [path:line] Title
- Description: What is wrong and why it matters
- Suggestion: Specific fix with code example
- Metadata: cross_lens_candidate=true/false, tradeoff_required=true/false
## Observations
- [MEDIUM|LOW] [path:line] Title
- Description: Informational finding
- Metadata: cross_lens_candidate=true/false, tradeoff_required=true/false
## Summary
[One paragraph overall assessment]
Severity Scale
- CRITICAL: Patterns that can cause cascading failures, silent data corruption, or unrecoverable state — e.g., swallowed errors in paths that mutate persistent state; unbounded I/O that can exhaust connection pools; retries of non-idempotent operations
- HIGH: Patterns likely to cause production incidents or lost observability — missing timeouts on external calls; catches that discard error context; partial-failure branches without rollback
- MEDIUM: Patterns that degrade diagnosability or robustness under load — log-and-return without re-raising; missing backoff on retries; narrow-but-still-overbroad catches
- LOW: Minor hygiene — inconsistent error wrapping, cosmetic improvements
Metadata Guidance Tags
cross_lens_candidate:
true when the finding also implicates another lens (e.g., silent swallow of a rollback-relevant error → also ARCHITECTURE; error message leakage to user → also SECURITY)
false when purely an error-handling concern
tradeoff_required:
true when the fix changes observable behavior (e.g., raising instead of returning null may break callers)
false when the fix is drop-in (e.g., adding a timeout constant)
What to Review
-
Error Signalling Discipline
- Return values that encode failure but are discarded: in exception-based languages, non-bang variants of persistence or parse operations whose falsy/null return is ignored (e.g., Rails
user.save without ! followed by logic that assumes persistence; Go err returned but not checked; Rust Result wrapped in let _ = ...; Node callback err parameter unused; C/POSIX return codes ignored).
- Operations that can fail but whose failure path is not represented in the code at all (e.g., an
unwrap/!!/try! in production code; a cast that can throw in a hot path with no handler above).
- Partial results returned as success (e.g., a function that returns
[] on network failure, silently collapsing "nothing matched" and "we couldn't ask" into the same value).
-
Exception / Error Hygiene
- Overbroad catches: catching the root error type (
Exception, Throwable, any, bare rescue, catch (...), Python except: with no type) without a justification comment and without re-raising.
- Empty catches: any catch block with no body, no log, and no explanatory comment.
- Catch-and-log-only where the caller expects the operation to have succeeded — turns a hard failure into a silent ghost.
- Lost context: catching an exception and raising a new one without attaching the original (no
raise Wrapped from e, no cause chain, no inner_exception).
- Rescue/catch scope too wide: wrapping many statements so the handler cannot tell which one failed.
-
Timeout and Cancellation Discipline
- External I/O without an explicit timeout: HTTP clients, DB drivers, queue/broker clients, cache clients, subprocess/exec calls, socket reads, file locks. Flag when the call uses defaults and the ambient default is either "infinite" or "library default that may be infinite".
- Missing cancellation propagation in languages with first-class cancellation (Go
context.Context, .NET CancellationToken, Kotlin coroutines CoroutineScope, Rust async abort handles). A long-running call that ignores an incoming cancellation signal will outlive its caller.
- Timeouts that are effectively infinite (e.g., hours on a user-facing request path) or absent on retry loops (retry body never times out, only the per-attempt call).
-
Retry Semantics
- Unbounded retries (no max attempts).
- Retries without backoff / jitter, which amplify thundering-herd and failure-cascade patterns.
- Retries of non-idempotent operations without an idempotency key, a pre-check, or a natural de-dup mechanism — each retry may cause real-world side effects (charge a card twice, send duplicate email, create duplicate records).
- Retrying on error classes that cannot succeed on replay (e.g., validation errors, 400-class responses, "not found" from external services).
-
Partial-Failure Handling
- Batch / multi-step operations that proceed on per-item failure without recording which items failed, and without either an all-or-nothing rollback or an explicit partial-success contract to the caller.
- Fan-out calls (parallel external requests) that ignore per-branch failure because the aggregator only awaits success.
- Multi-writer flows where one store succeeds and another fails with no compensating action and no reconciliation path.
-
Error-Context Observability
- Caught errors logged as text-only (
logger.warn("something failed")) with no error object, no stack, no correlation/request id, no operation name, no affected identifiers.
- Errors reported to an error-tracking system (Sentry/Rollbar/Datadog/Bugsnag/Honeybadger/etc.) without tags/extras that would let on-call triage.
- Errors rescued-and-raised-differently such that the error tracker sees only the outer wrapper and loses the original.
-
Resource Cleanup on Error Paths
- Resources acquired without a structured release pattern on failure: open files, DB connections, locks, file descriptors, sockets, temp directories, mutexes.
- Languages/patterns to look for: try/finally;
with / using / RAII / defer; "ensure" blocks; bracket patterns in FP languages; scoped resource wrappers.
- Flag when an acquire call is followed by work that can throw but the release is not in a finalizing scope.
-
Defensive-Programming Overreach (anti-finding)
- Report as LOW/Observation: try/catch wrapping code that cannot fail; null-guards on values the type system proves non-null; re-validation of values already validated upstream within the same trust boundary.
- This is real code smell but is not a production incident source — keep it LOW so it does not crowd out genuine resilience findings.
Language / Framework Triggers (non-exhaustive)
The lens applies wherever code performs I/O, mutates shared state, or calls across a trust boundary. Example triggers per ecosystem:
- Exception-based: Python
except:; Java/Kotlin catch (Exception; C# catch (Exception; JavaScript/TypeScript try/catch; Ruby rescue / bang-methods; PHP catch (\Throwable.
- Return-value based: Go
if err != nil, unused _ returns; Rust Result + .unwrap()/.expect(); C/C++ errno/return-code; Node.js error-first callbacks.
- Timeout APIs: HTTP clients, DB drivers, queue clients, cache clients, subprocess invocations — check for an explicit timeout argument or wrapper.
- Retry libraries: exponential backoff, circuit breakers, bulkheads (Nygard patterns).
- Cancellation: Go
context, .NET CancellationToken, JS AbortSignal, Kotlin coroutines, Rust async runtimes.
When reviewing code that performs I/O but does not use one of the above patterns, that is itself a finding.
Example Findings
## Must Fix
- [HIGH] [path/to/service.ext:N] External HTTP call lacks timeout
- Description: The call to the downstream service uses the library default, which is unbounded on this client. A slow or hung upstream will exhaust the connection pool and cascade to this service's request queue.
- Suggestion: Pass an explicit request-level timeout (both connect and read). Wrap in a circuit-breaker or per-call retry with backoff if transient failures are expected.
- Metadata: cross_lens_candidate=true, tradeoff_required=false
- [CRITICAL] [path/to/job.ext:N] Non-idempotent side effect inside unbounded retry
- Description: The retry loop calls a payment API on each attempt without an idempotency key and without a pre-check. On partial success (HTTP 5xx after the charge succeeds), the customer may be charged multiple times.
- Suggestion: Pass an idempotency key derived from the request/job id; treat non-2xx-non-4xx as transient; cap attempts.
- Metadata: cross_lens_candidate=false, tradeoff_required=true
Adversarial Input Discipline
Before returning any verdict, pick ONE external dependency or fallible operation the diff introduces or modifies, and trace what happens if it:
- Times out (hangs beyond any expected latency).
- Fails with a transient error (5xx / network blip / deadlock retry signal).
- Fails with a permanent error (4xx / validation / "not found").
- Succeeds on attempt N after N-1 silent failures on the same caller's behalf.
For each scenario, state the resulting system state: what is logged, what is retried, what is persisted, what the caller observes, and whether any resource leaks. If you cannot construct these four scenarios for the primary fallible operation, your review is incomplete — return BLOCKED with reason "could not trace failure scenarios" rather than APPROVED.
Trace cross-file: when a caught error is re-raised, logged, or returned to a caller, follow it to at least the next layer. Silent loss of an error at any layer is a finding regardless of which file the loss occurs in.
Integration Notes
This lens is part of the multi-lens code review protocol. Findings are merged with other lenses via deterministic synthesis:
- Dedupe by (path, line, title)
- Precedence: SECURITY > ERROR_HANDLING > TYPE_SAFETY > PERFORMANCE > ARCHITECTURE > SIMPLICITY
- Any CRITICAL finding from this lens blocks APPROVED verdict