SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# Patterns — Handling Errors

# Contents

  • Error taxonomy skeleton (Python)
  • Boundary handler → problem+json
  • Retry with exponential backoff + full jitter
  • Circuit breaker
  • Fail-fast startup
  • Gotchas

# Error taxonomy skeleton (Python)

python
class AppError(Exception):
    status = 500
    title = "Internal error"
    def __init__(self, detail="", **ctx):
        super().__init__(detail)
        self.detail, self.ctx = detail, ctx

class ValidationError(AppError): status, title = 400, "Invalid request"
class AuthError(AppError):       status, title = 401, "Authentication required"
class Forbidden(AppError):       status, title = 403, "Not allowed"
class NotFound(AppError):        status, title = 404, "Resource not found"
class Conflict(AppError):        status, title = 409, "Conflict"
class RateLimited(AppError):     status, title = 429, "Too many requests"

# Boundary handler → problem+json

One handler at the HTTP edge; nothing below it formats responses.

python
@app.exception_handler(AppError)
def app_error(request, exc):
    # Exactly one log line per failure, with context, at the boundary.
    logger.warning("request_failed", status=exc.status,
                   error=type(exc).__name__, **exc.ctx)
    return JSONResponse(status_code=exc.status, media_type="application/problem+json",
        content={"type": f"https://api.example.com/errors/{type(exc).__name__}",
                 "title": exc.title, "status": exc.status,
                 "detail": exc.detail, "instance": str(request.url.path)})

@app.exception_handler(Exception)
def unexpected(request, exc):
    logger.exception("unhandled_error")          # full trace to logs only
    return JSONResponse(status_code=500, media_type="application/problem+json",
        content={"title": "Internal error", "status": 500,
                 "detail": "Unexpected error. Retry or contact support."})

# Retry with exponential backoff + full jitter

python
import random, time

# base 0.2s, factor 2, cap 30s, 5 attempts: worst-case wait ~ <60s total.
def retry(fn, retryable=(TimeoutError, ConnectionError),
          attempts=5, base=0.2, cap=30.0):
    for n in range(attempts):
        try:
            return fn()
        except retryable:
            if n == attempts - 1:
                raise
            time.sleep(random.uniform(0, min(cap, base * 2 ** n)))  # full jitter

Honor server hints: if the response carries Retry-After: N, sleep N seconds instead of the computed backoff.

# Circuit breaker

python
class Breaker:
    # 5 consecutive failures opens; probe after 30s (half-open).
    def __init__(self, threshold=5, reset_after=30.0):
        self.fail, self.threshold, self.reset_after = 0, threshold, reset_after
        self.opened_at = None
    def call(self, fn):
        if self.opened_at is not None:
            if time.monotonic() - self.opened_at < self.reset_after:
                raise DependencyDown("circuit open")
            self.opened_at = None            # half-open: allow one probe
        try:
            out = fn()
        except Exception:
            self.fail += 1
            if self.fail >= self.threshold:
                self.opened_at = time.monotonic()
            raise
        self.fail = 0
        return out

# Fail-fast startup

python
def main():
    cfg = load_config()          # raises with the missing key named
    db.ping(cfg.database_url)    # unreachable DB -> crash now, not at first request
    run_pending_migration_check(cfg)
    serve(cfg)

# Gotchas

  • except Exception: pass hides bugs for months — if a failure is truly ignorable, log it at DEBUG with a reason string.
  • Re-raising with raise NewError(...) from e preserves the chain; bare raise NewError(...) destroys the original traceback.
  • Retrying a POST without an idempotency key can double-charge/double-create — timeouts are unknown outcome, not failure.
  • Breakers per dependency, not global — one dead cache must not open the DB breaker.
  • problem+json type URLs should be stable identifiers; they don't have to resolve, but never reuse one for a different meaning.
  • 500 bodies must be static — rendering them from the exception risks leaking internals and can itself fail.