AgentStack
SKILL verified MIT Self-run

Error Handling

skill-kid-sid-claude-spellbook-error-handling · by kid-sid

Use when designing error hierarchies, propagating errors across service boundaries, implementing retry and backoff logic, writing structured error responses for APIs, or making error handling consistent across a Python, TypeScript, or Go codebase.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-kid-sid-claude-spellbook-error-handling

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Error Handling? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Error Handling

Patterns for structured errors, propagation, retries, and consistent failure surfaces across Python, TypeScript, and Go.

When to Activate

  • Designing a custom exception or error hierarchy for a service
  • Propagating errors across HTTP, gRPC, or queue boundaries
  • Implementing retry logic with exponential backoff and jitter
  • Writing consistent error responses for a REST or GraphQL API
  • Distinguishing recoverable from unrecoverable errors
  • Auditing a codebase for swallowed exceptions or bare except blocks
  • Adding context to errors without losing the original cause

Error Classification

Classify errors before deciding how to handle them — the right response depends on what kind of failure occurred.

| Class | Examples | Handle by | |---|---|---| | Validation | Missing field, bad format | Return 400 immediately — no retry | | Not found | Unknown ID, deleted resource | Return 404 — no retry | | Auth | Invalid token, expired session | Return 401/403 — no retry | | Transient | Network timeout, DB connection blip | Retry with backoff | | Dependency | Third-party API down, queue unavailable | Retry, then circuit-break | | Logic bug | Null deref, assertion failure | Log + alert — do not retry or swallow | | Rate limited | 429 from upstream | Retry after Retry-After header |


Custom Error Hierarchies

Python

# Base domain error — all app errors inherit from this
class AppError(Exception):
    def __init__(self, message: str, code: str, status: int = 500) -> None:
        super().__init__(message)
        self.code = code
        self.status = status

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str) -> None:
        super().__init__(f"{resource} '{id}' not found", "NOT_FOUND", 404)

class ValidationError(AppError):
    def __init__(self, field: str, reason: str) -> None:
        super().__init__(f"Invalid '{field}': {reason}", "VALIDATION_ERROR", 400)

class TransientError(AppError):
    """Signals a retry is safe."""
    def __init__(self, message: str) -> None:
        super().__init__(message, "TRANSIENT_ERROR", 503)

TypeScript

// Tagged union — exhaustive handling without instanceof chains
type AppError =
  | { tag: "NOT_FOUND"; resource: string; id: string }
  | { tag: "VALIDATION"; field: string; reason: string }
  | { tag: "UNAUTHORIZED" }
  | { tag: "TRANSIENT"; message: string; retryable: true };

function notFound(resource: string, id: string): AppError {
  return { tag: "NOT_FOUND", resource, id };
}

// Result type — explicit error path, no throw/catch in business logic
type Result = { ok: true; value: T } | { ok: false; error: E };

function ok(value: T): Result { return { ok: true, value }; }
function err(error: E): Result { return { ok: false, error }; }

// Usage
async function getUser(id: string): Promise> {
  const user = await db.findUser(id);
  if (!user) return err(notFound("User", id));
  return ok(user);
}

const result = await getUser("123");
if (!result.ok) {
  // result.error is fully typed here
  switch (result.error.tag) {
    case "NOT_FOUND": return res.status(404).json({ error: result.error });
    case "UNAUTHORIZED": return res.status(401).json({ error: "Unauthorized" });
  }
}

Go

// Sentinel errors for classification
var (
    ErrNotFound   = errors.New("not found")
    ErrValidation = errors.New("validation error")
    ErrTransient  = errors.New("transient error")
)

// Structured error with context
type AppError struct {
    Code    string
    Message string
    Err     error  // wrapped cause
}

func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Err }

func NotFound(resource, id string) *AppError {
    return &AppError{
        Code:    "NOT_FOUND",
        Message: fmt.Sprintf("%s '%s' not found", resource, id),
        Err:     ErrNotFound,
    }
}

// Propagate with context — never discard the original error
func getUser(ctx context.Context, id string) (*User, error) {
    user, err := db.FindUser(ctx, id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, NotFound("User", id)
        }
        return nil, fmt.Errorf("getUser %s: %w", id, err)
    }
    return user, nil
}

// Check error class at the boundary
if errors.Is(err, ErrNotFound) {
    http.Error(w, "Not found", http.StatusNotFound)
    return
}
if errors.Is(err, ErrTransient) {
    http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
    return
}

Adding Context Without Losing the Cause

# Python — chain exceptions
try:
    result = db.query(sql)
except psycopg2.OperationalError as exc:
    raise TransientError("database unavailable") from exc  # preserves __cause__

# BAD — swallows the original traceback
raise TransientError("database unavailable")
// TypeScript — wrap with cause
throw new Error("Database unavailable", { cause: originalError });

// BAD — loses original stack
throw new Error("Database unavailable");
// Go — %w verb wraps for errors.Is / errors.As
return fmt.Errorf("fetchOrders: %w", err)   // ✓ preserves chain

// BAD — breaks unwrapping
return fmt.Errorf("fetchOrders: %v", err)   // ✗ wraps as string only
return errors.New("fetchOrders failed")     // ✗ discards original

Retry with Exponential Backoff and Jitter

Jitter prevents thundering herd when many callers retry simultaneously.

# Python
import asyncio, random

async def with_retry(fn, *, max_attempts: int = 3, base_delay: float = 0.5):
    for attempt in range(max_attempts):
        try:
            return await fn()
        except TransientError:
            if attempt == max_attempts - 1:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.3)
            await asyncio.sleep(delay)
// TypeScript
async function withRetry(
  fn: () => Promise,
  { maxAttempts = 3, baseDelay = 500 } = {}
): Promise {
  for (let attempt = 0; attempt  setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}

function isRetryable(err: unknown): boolean {
  return err instanceof AppError && err.tag === "TRANSIENT";
}
// Go
func WithRetry(ctx context.Context, fn func() error, maxAttempts int) error {
    var err error
    for attempt := range maxAttempts {
        if err = fn(); err == nil {
            return nil
        }
        if !errors.Is(err, ErrTransient) {
            return err
        }
        if attempt == maxAttempts-1 {
            break
        }
        delay := time.Duration(math.Pow(2, float64(attempt))*500)*time.Millisecond +
            time.Duration(rand.Intn(300))*time.Millisecond
        select {
        case  JSONResponse:
    return JSONResponse(
        status_code=exc.status,
        content={
            "error": {
                "code": exc.code,
                "message": str(exc),
                "request_id": request.state.request_id,
            }
        },
    )
// Express global error handler — must be last middleware
app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
  const requestId = req.headers["x-request-id"] ?? crypto.randomUUID();
  if (err instanceof AppError) {
    return res.status(err.status).json({
      error: { code: err.code, message: err.message, request_id: requestId },
    });
  }
  console.error("Unhandled error", err);
  res.status(500).json({
    error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred", request_id: requestId },
  });
});

Logging Errors

| Severity | When | Fields to include | |---|---|---| | ERROR | Unhandled or unexpected failures | message, stack, requestid, userid | | WARN | Handled but notable (retry succeeded, degraded mode) | message, attempt count | | INFO | Expected failures returned to caller (404, 400) | code, path — no stack trace | | DEBUG | Retry attempts, circuit state | attempt, delay, error message |

# BAD — log and re-raise doubles the noise
except SomeError as exc:
    logger.error("Failed", exc_info=True)
    raise  # logged again by the global handler

# GOOD — log once at the boundary, propagate elsewhere
except SomeError as exc:
    raise TransientError("upstream failed") from exc  # logged once at the top

Red Flags

  • Bare except / catch (e) {} — swallows the error entirely; always re-raise or handle explicitly.
  • Retrying non-retryable errors — retrying a 400 or 404 wastes time and can amplify load; check error class before retrying.
  • raise Exception("something went wrong") — generic base types give callers no way to branch; use typed domain errors.
  • Logging and re-raising at every layer — logs the same error multiple times; propagate up and log once at the boundary.
  • Using %v instead of %w in Go — breaks errors.Is / errors.As unwrapping; always use %w when wrapping.
  • Ignoring err in Go_ = fn() silently discards failures; use //nolint:errcheck only with a documented reason.
  • No request_id in error responses — makes support and debugging impossible; always include a correlation ID.
  • Returning 500 for client errors — masks bugs; map domain errors to the correct 4xx before they reach the handler.
  • Retry without jitter — synchronized retries hammer a recovering dependency; always add random jitter.

Checklist

  • [ ] Custom error types carry code, message, and HTTP status — no raw Exception("string") at boundaries
  • [ ] Errors are wrapped with context at each layer (from exc / { cause } / %w) — original cause preserved
  • [ ] Error class determined before retrying — only TRANSIENT errors retried, never validation or auth errors
  • [ ] Retry uses exponential backoff with jitter — no fixed-interval loops
  • [ ] Global error handler present — no unhandled errors reaching the framework default
  • [ ] API error responses include code, message, and request_id — consistent shape across all endpoints
  • [ ] Errors logged once at the top boundary — not re-logged at every layer
  • [ ] INFO used for expected 4xx, ERROR for unexpected 5xx — no stack traces in 404 logs
  • [ ] Go functions never ignore error return values without a documented reason
  • [ ] Circuit breaker or fallback present for calls to external dependencies

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.