mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-19 15:23:03 +08:00
docs: salvage agent and motion workflow skills
This commit is contained in:
committed by
Affaan Mustafa
parent
60782502d5
commit
629d4c0c61
376
skills/error-handling/SKILL.md
Normal file
376
skills/error-handling/SKILL.md
Normal file
@@ -0,0 +1,376 @@
|
||||
---
|
||||
name: error-handling
|
||||
description: Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Error Handling Patterns
|
||||
|
||||
Consistent, robust error handling patterns for production applications.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing error types or exception hierarchies for a new module or service
|
||||
- Adding retry logic or circuit breakers for unreliable external dependencies
|
||||
- Reviewing API endpoints for missing error handling
|
||||
- Implementing user-facing error messages and feedback
|
||||
- Debugging cascading failures or silent error swallowing
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Fail fast and loudly** — surface errors at the boundary where they occur; don't bury them
|
||||
2. **Typed errors over string messages** — errors are first-class values with structure
|
||||
3. **User messages ≠ developer messages** — show friendly text to users, log full context server-side
|
||||
4. **Never swallow errors silently** — every `catch` block must either handle, re-throw, or log
|
||||
5. **Errors are part of your API contract** — document every error code a client may receive
|
||||
|
||||
## TypeScript / JavaScript
|
||||
|
||||
### Typed Error Classes
|
||||
|
||||
```typescript
|
||||
// Define an error hierarchy for your domain
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: string,
|
||||
public readonly statusCode: number = 500,
|
||||
public readonly details?: unknown,
|
||||
) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
// Maintain correct prototype chain in transpiled ES5 JavaScript.
|
||||
// Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)
|
||||
// to work correctly when extending the built-in Error class.
|
||||
Object.setPrototypeOf(this, new.target.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(resource: string, id: string) {
|
||||
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message: string, details: { field: string; message: string }[]) {
|
||||
super(message, 'VALIDATION_ERROR', 422, details)
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends AppError {
|
||||
constructor(reason = 'Authentication required') {
|
||||
super(reason, 'UNAUTHORIZED', 401)
|
||||
}
|
||||
}
|
||||
|
||||
export class RateLimitError extends AppError {
|
||||
constructor(public readonly retryAfterMs: number) {
|
||||
super('Rate limit exceeded', 'RATE_LIMITED', 429)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Result Pattern (no-throw style)
|
||||
|
||||
For operations where failure is expected and common (parsing, external calls):
|
||||
|
||||
```typescript
|
||||
type Result<T, E = AppError> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E }
|
||||
|
||||
function ok<T>(value: T): Result<T> {
|
||||
return { ok: true, value }
|
||||
}
|
||||
|
||||
function err<E>(error: E): Result<never, E> {
|
||||
return { ok: false, error }
|
||||
}
|
||||
|
||||
// Usage
|
||||
async function fetchUser(id: string): Promise<Result<User>> {
|
||||
try {
|
||||
const user = await db.users.findUnique({ where: { id } })
|
||||
if (!user) return err(new NotFoundError('User', id))
|
||||
return ok(user)
|
||||
} catch (e) {
|
||||
return err(new AppError('Database error', 'DB_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
const result = await fetchUser('abc-123')
|
||||
if (!result.ok) {
|
||||
// TypeScript knows result.error here
|
||||
logger.error('Failed to fetch user', { error: result.error })
|
||||
return
|
||||
}
|
||||
// TypeScript knows result.value here
|
||||
console.log(result.value.email)
|
||||
```
|
||||
|
||||
### API Error Handler (Next.js / Express)
|
||||
|
||||
```typescript
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
function handleApiError(error: unknown): NextResponse {
|
||||
// Known application error
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
...(error.details ? { details: error.details } : {}),
|
||||
},
|
||||
},
|
||||
{ status: error.statusCode },
|
||||
)
|
||||
}
|
||||
|
||||
// Zod validation error
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Request validation failed',
|
||||
details: error.issues.map(i => ({
|
||||
field: i.path.join('.'),
|
||||
message: i.message,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{ status: 422 },
|
||||
)
|
||||
}
|
||||
|
||||
// Unexpected error — log details, return generic message
|
||||
console.error('Unexpected error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// ... handler logic
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React Error Boundary
|
||||
|
||||
```typescript
|
||||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
fallback: ReactNode
|
||||
onError?: (error: Error, info: ErrorInfo) => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false, error: null }
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
this.props.onError?.(error, info)
|
||||
console.error('Unhandled React error:', error, info)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) return this.props.fallback
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
<ErrorBoundary fallback={<p>Something went wrong. Please refresh.</p>}>
|
||||
<MyComponent />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
### Custom Exception Hierarchy
|
||||
|
||||
```python
|
||||
class AppError(Exception):
|
||||
"""Base application error."""
|
||||
def __init__(self, message: str, code: str, status_code: int = 500):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.status_code = status_code
|
||||
|
||||
class NotFoundError(AppError):
|
||||
def __init__(self, resource: str, id: str):
|
||||
super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
|
||||
|
||||
class ValidationError(AppError):
|
||||
def __init__(self, message: str, details: list[dict] | None = None):
|
||||
super().__init__(message, "VALIDATION_ERROR", 422)
|
||||
self.details = details or []
|
||||
```
|
||||
|
||||
### FastAPI Global Exception Handler
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.exception_handler(AppError)
|
||||
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": {"code": exc.code, "message": str(exc)}},
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
# Log full details, return generic message
|
||||
logger.exception("Unexpected error", exc_info=exc)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}},
|
||||
)
|
||||
```
|
||||
|
||||
## Go
|
||||
|
||||
### Sentinel Errors and Error Wrapping
|
||||
|
||||
```go
|
||||
package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
// Sentinel errors for type-checking
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrConflict = errors.New("conflict")
|
||||
)
|
||||
|
||||
// Wrap errors with context — never lose the original
|
||||
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
|
||||
user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying user %s: %w", id, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// At the handler level, unwrap to determine response
|
||||
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNotFound):
|
||||
writeError(w, http.StatusNotFound, "not_found", err.Error())
|
||||
case errors.Is(err, domain.ErrUnauthorized):
|
||||
writeError(w, http.StatusForbidden, "forbidden", "Access denied")
|
||||
default:
|
||||
slog.Error("unexpected error", "err", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "An unexpected error occurred")
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, user)
|
||||
}
|
||||
```
|
||||
|
||||
## Retry with Exponential Backoff
|
||||
|
||||
```typescript
|
||||
interface RetryOptions {
|
||||
maxAttempts?: number
|
||||
baseDelayMs?: number
|
||||
maxDelayMs?: number
|
||||
retryIf?: (error: unknown) => boolean
|
||||
}
|
||||
|
||||
async function withRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: RetryOptions = {},
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
baseDelayMs = 500,
|
||||
maxDelayMs = 10_000,
|
||||
retryIf = () => true,
|
||||
} = options
|
||||
|
||||
let lastError: unknown
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
if (attempt === maxAttempts || !retryIf(error)) throw error
|
||||
|
||||
const jitter = Math.random() * baseDelayMs
|
||||
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
// Usage: retry transient network errors, not 4xx
|
||||
const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
|
||||
maxAttempts: 3,
|
||||
retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
|
||||
})
|
||||
```
|
||||
|
||||
## User-Facing Error Messages
|
||||
|
||||
Map error codes to human-readable messages. Keep technical details out of user-visible text.
|
||||
|
||||
```typescript
|
||||
const USER_ERROR_MESSAGES: Record<string, string> = {
|
||||
NOT_FOUND: 'The requested item could not be found.',
|
||||
UNAUTHORIZED: 'Please sign in to continue.',
|
||||
FORBIDDEN: "You don't have permission to do that.",
|
||||
VALIDATION_ERROR: 'Please check your input and try again.',
|
||||
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
|
||||
INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',
|
||||
}
|
||||
|
||||
export function getUserMessage(code: string): string {
|
||||
return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Checklist
|
||||
|
||||
Before merging any code that touches error handling:
|
||||
|
||||
- [ ] Every `catch` block handles, re-throws, or logs — no silent swallowing
|
||||
- [ ] API errors follow the standard envelope `{ error: { code, message } }`
|
||||
- [ ] User-facing messages contain no stack traces or internal details
|
||||
- [ ] Full error context is logged server-side
|
||||
- [ ] Custom error classes extend a base `AppError` with a `code` field
|
||||
- [ ] Async functions surface errors to callers — no fire-and-forget without fallback
|
||||
- [ ] Retry logic only retries retriable errors (not 4xx client errors)
|
||||
- [ ] React components are wrapped in `ErrorBoundary` for rendering errors
|
||||
Reference in New Issue
Block a user