From claudster
Error handling patterns for Python and TypeScript applications. Use when designing error hierarchies, implementing retry logic, building error boundaries, or establishing logging strategies. Covers custom exceptions, result types, circuit breakers, and user-facing error messages.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claudster:error-handlingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Errors are not exceptional — they are part of the contract. This skill defines how to classify, propagate, and recover from errors consistently.
Errors are not exceptional — they are part of the contract. This skill defines how to classify, propagate, and recover from errors consistently.
Trigger conditions:
except: or swallowed errorsEvery error falls into one of three categories. Handle each differently.
| Category | Retryable? | User Action? | Example |
|---|---|---|---|
| Transient | Yes | None (auto-retry) | Network timeout, DB connection dropped, rate limit |
| Operational | No | Inform user | Invalid input, resource not found, insufficient permissions |
| Fatal | No | Alert team | Corrupted state, config missing, unrecoverable assertion |
Build domain-specific exceptions. Never raise raw Exception.
class AppError(Exception):
"""Base error for the application."""
def __init__(self, message: str, code: str | None = None):
self.message = message
self.code = code or self.__class__.__name__
super().__init__(self.message)
# Operational errors (expected, handled gracefully)
class NotFoundError(AppError):
"""Resource does not exist."""
class ValidationError(AppError):
"""Input validation failed."""
class AuthorizationError(AppError):
"""User not authorized for this action."""
# Transient errors (retry-eligible)
class TransientError(AppError):
"""Temporary failure — safe to retry."""
class ExternalServiceError(TransientError):
"""External API call failed."""
class DatabaseConnectionError(TransientError):
"""Database connection lost."""
# ✅ GOOD: Specific exception with context
def get_user(user_id: str) -> User:
user = repo.find_by_id(user_id)
if user is None:
raise NotFoundError(f"User {user_id} not found")
return user
# ✅ GOOD: Catch specific, re-raise as domain error
def fetch_external_data(url: str) -> dict:
try:
response = httpx.get(url, timeout=10)
response.raise_for_status()
return response.json()
except httpx.TimeoutException as e:
raise ExternalServiceError(f"Timeout calling {url}") from e
except httpx.HTTPStatusError as e:
raise ExternalServiceError(f"HTTP {e.response.status_code} from {url}") from e
# ❌ BAD: Bare except swallows everything
try:
result = process()
except:
pass
# ❌ BAD: Catching too broadly
try:
result = process()
except Exception:
logger.error("Something went wrong")
return None # Hides the real error
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseConfig(raw: string): Result<Config, ValidationError> {
try {
const parsed = JSON.parse(raw);
if (!isValidConfig(parsed)) {
return { ok: false, error: new ValidationError("Invalid config shape") };
}
return { ok: true, value: parsed as Config };
} catch {
return { ok: false, error: new ValidationError("Invalid JSON") };
}
}
// Usage — caller must handle both cases
const result = parseConfig(rawInput);
if (!result.ok) {
showError(result.error.message);
return;
}
useConfig(result.value);
'use client';
import { Component, type ReactNode } from 'react';
interface Props { children: ReactNode; fallback: ReactNode; }
interface State { hasError: boolean; }
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Component error:', error, info.componentStack);
// Report to error tracking service
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
import asyncio
import random
from loguru import logger
async def retry_with_backoff(
fn,
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
retryable_exceptions: tuple = (TransientError,),
):
"""Retry a function with exponential backoff and jitter."""
for attempt in range(max_retries + 1):
try:
return await fn()
except retryable_exceptions as e:
if attempt == max_retries:
logger.error(f"All {max_retries} retries exhausted: {e}")
raise
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
logger.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay:.1f}s")
await asyncio.sleep(delay + jitter)
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from loguru import logger
app = FastAPI()
@app.exception_handler(NotFoundError)
async def not_found_handler(request: Request, exc: NotFoundError):
return JSONResponse(status_code=404, content={"error": exc.code, "message": exc.message})
@app.exception_handler(ValidationError)
async def validation_handler(request: Request, exc: ValidationError):
return JSONResponse(status_code=422, content={"error": exc.code, "message": exc.message})
@app.exception_handler(AuthorizationError)
async def auth_handler(request: Request, exc: AuthorizationError):
return JSONResponse(status_code=403, content={"error": exc.code, "message": exc.message})
@app.exception_handler(Exception)
async def unhandled_handler(request: Request, exc: Exception):
logger.exception(f"Unhandled error on {request.method} {request.url}")
# Never expose internal details to the client
return JSONResponse(status_code=500, content={"error": "InternalError", "message": "An internal error occurred."})
from loguru import logger
# ✅ GOOD: Log with context, not just the message
logger.error(f"Failed to process order {order_id}: {e}", order_id=order_id, customer_id=customer_id)
# ✅ GOOD: Use exception() to capture stack trace
try:
process_order(order)
except Exception as e:
logger.exception(f"Order processing failed for {order.id}")
raise
# ❌ BAD: Logging sensitive data
logger.error(f"Auth failed for {username} with password {password}")
# ❌ BAD: Logging without context
logger.error("Something failed")
| Audience | Include | Exclude |
|---|---|---|
| End user | What happened, what to do next | Stack traces, internal IDs, SQL errors |
| Developer (logs) | Full context, request ID, stack trace | User passwords, tokens, PII |
| API consumer | Error code, human message, field-level detail | Server internals, file paths |
# User-facing message
"Unable to load your dashboard. Please try again in a few minutes."
# API response
{"error": "ValidationError", "message": "Email format is invalid", "field": "email"}
# Log entry
"Validation failed on POST /api/users: email='not-an-email' — rejected by regex validator"
| Anti-Pattern | Problem | Fix |
|---|---|---|
Bare except: / except Exception: | Catches KeyboardInterrupt, SystemExit | Catch specific exceptions |
return None on error | Caller has no idea what went wrong | Raise typed exception |
| Logging and re-raising | Double logging, noisy | Log at the boundary, not at every layer |
| String error codes | Typos, no autocomplete | Enum or class-based error codes |
| Swallowing errors in background tasks | Silent failures, data loss | Log + alert + dead-letter queue |
| Generic "Something went wrong" | User can't self-serve | Specific message + action hint |
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub saajunaid/junai --plugin claudster