error-handling
Architect and implement resilient error handling, custom error hierarchies, and recovery strategies across application boundaries. Use when distinguishing transient from permanent failures, designing
SKILL.mdskills/error-handling/SKILL.md
GitHubResilient Error Handling and Recovery
Software systems inevitably encounter unexpected states: network outages, corrupted input, timeouts, and resource exhaustion. Robust engineering categorizes errors systematically, propagates context cleanly, applies resilient retry strategies for transient faults, and enforces strict error boundaries to prevent cascading crashes.
Core Rules
- Distinguish transient from permanent failures: Transient errors (network blips, rate limits, deadlock contention) should be retried with exponential backoff; permanent errors (validation failures, 404 Not Found, bad auth) must fail fast.
- Never catch and silently ignore exceptions: Catching an error without logging, alerting, or re-raising hides bugs and corrupts system state.
- Preserve the error cause chain: In exceptions, always chain the root cause (
raise CustomError from original_err). In Result types, bundle the underlying error type. - Always use jitter with exponential backoff: Retrying at exact intervals causes a "thundering herd" effect that overwhelms recovering services. Add randomized jitter.
- Sanitize error messages at system boundaries: Never leak internal database schemas, stack traces, or credentials in client-facing HTTP/API responses.
1. Error Classification Matrix
| Error Class | Characteristics | Examples | Recommended Action |
|---|---|---|---|
| Transient | Temporary external condition; likely to succeed on subsequent attempt | HTTP 503, 429 Rate Limit, TCP socket timeout, DB deadlock | Retry with exponential backoff & jitter |
| Permanent Client | Bad input or protocol violation; will fail every time with same payload | HTTP 400 Bad Request, validation error, schema mismatch | Fail immediately; return clear diagnostic error to client |
| Permanent System | Internal bug or severe infrastructure failure | Out of Memory, missing binary, disk hardware error | Fail fast; log critical alert; abort process if state is corrupted |
2. Exponential Backoff with Jitter
The industry standard algorithm for retrying network operations safely:
import random
import time
import logging
logger = logging.getLogger(__name__)
def retry_with_backoff(
func,
max_retries: int = 4,
base_delay: float = 0.5,
max_delay: float = 10.0,
):
"""Execute func with full jitter exponential backoff."""
attempt = 0
while True:
try:
return func()
except TransientNetworkError as exc:
attempt += 1
if attempt > max_retries:
logger.error("Operation failed after %d retries: %s", max_retries, exc)
raise
# Full jitter formula: sleep = uniform(0, min(max_delay, base_delay * 2^attempt))
calculated_backoff = min(max_delay, base_delay * (2 ** (attempt - 1)))
sleep_duration = random.uniform(0, calculated_backoff)
logger.warning(
"Transient error on attempt %d/%d: %s. Backing off for %.2fs",
attempt, max_retries, exc, sleep_duration
)
time.sleep(sleep_duration)
3. Exceptions vs Result Types
Choose the error idiom that fits your programming language:
Result / Either Pattern (Rust, Go, TypeScript)
Used when errors are an expected part of normal domain operations (e.g. parsing, validation):
type Result<T, E> =
| { success: true; value: T }
| { success: false; error: E };
function parsePort(input: string): Result<number, string> {
const num = parseInt(input, 10);
if (isNaN(num) || num < 1 || num > 65535) {
return { success: false, error: `Invalid port number: ${input}` };
}
return { success: true, value: num };
}
Exception Hierarchies (Python, Java, C#)
Used for exceptional conditions, with domain-specific base classes:
class AppDomainError(Exception):
"""Base class for all domain errors."""
class EntityNotFoundError(AppDomainError):
def __init__(self, entity_name: str, entity_id: str):
super().__init__(f"{entity_name} with ID '{entity_id}' not found.")
self.entity_name = entity_name
self.entity_id = entity_id
4. Boundary Sanitization (Security)
At API or HTTP boundaries, translate internal exceptions into safe responses:
# FastAPI / Flask middleware boundary example
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
error_id = str(uuid.uuid4())
# Log internal details and stack trace privately:
logger.error("Unhandled error [ID: %s] on %s: %s", error_id, request.url, exc, exc_info=True)
# Return safe, non-leaking error to client:
return JSONResponse(
status_code=500,
content={
"error": "InternalServerError",
"message": "An unexpected error occurred. Please quote the reference ID.",
"reference_id": error_id,
},
)
5. Common Mistakes and Anti-Patterns
| Anti-Pattern | Risk | Correct Practice |
|---|---|---|
| Retrying permanent errors (e.g. HTTP 400 or 401) | Wastes server resources, triggers rate limits | Only retry transient faults (503, 429, timeouts) |
| Retrying without jitter (fixed sleep) | Synchronizes traffic bursts, crashing the database | Always add randomized jitter |
Catching bare Exception without re-raise |
Masks syntax errors, keyboard interrupts, OOM | Catch specific exception types |
| Leaking stack traces to HTTP responses | Exposes internal paths, library versions to attackers | Log internally; return generic message with reference ID |
6. Verification Checklist
- Transient faults are retried with exponential backoff and randomized jitter.
- Retries have a strict maximum limit (
max_retries <= 5). - Non-transient client errors fail immediately without retry.
- Error messages at API boundaries contain no internal stack traces or secrets.
- All chained exceptions preserve the original
fromcause.