Install
$ agentstack add skill-kid-sid-claude-spellbook-error-handling ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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
exceptblocks - 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
%vinstead of%win Go — breakserrors.Is/errors.Asunwrapping; always use%wwhen wrapping. - Ignoring
errin Go —_ = fn()silently discards failures; use//nolint:errcheckonly with a documented reason. - No
request_idin 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 HTTPstatus— no rawException("string")at boundaries - [ ] Errors are wrapped with context at each layer (
from exc/{ cause }/%w) — original cause preserved - [ ] Error class determined before retrying — only
TRANSIENTerrors 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, andrequest_id— consistent shape across all endpoints - [ ] Errors logged once at the top boundary — not re-logged at every layer
- [ ]
INFOused for expected 4xx,ERRORfor unexpected 5xx — no stack traces in 404 logs - [ ] Go functions never ignore
errorreturn 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.
- Author: kid-sid
- Source: kid-sid/claude-spellbook
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.