@champpaba/claude-agent-kit 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +49 -0
- package/.claude/commands/csetup.md +364 -38
- package/.claude/commands/cview.md +364 -364
- package/.claude/contexts/design/accessibility.md +611 -611
- package/.claude/contexts/design/layout.md +400 -400
- package/.claude/contexts/design/responsive.md +551 -551
- package/.claude/contexts/design/shadows.md +522 -522
- package/.claude/contexts/design/typography.md +465 -465
- package/.claude/contexts/domain/README.md +164 -164
- package/.claude/contexts/patterns/agent-coordination.md +388 -388
- package/.claude/contexts/patterns/development-principles.md +513 -513
- package/.claude/contexts/patterns/error-handling.md +478 -478
- package/.claude/contexts/patterns/logging.md +424 -424
- package/.claude/contexts/patterns/tdd-classification.md +516 -516
- package/.claude/contexts/patterns/testing.md +413 -413
- package/.claude/lib/tdd-classifier.md +345 -345
- package/.claude/lib/validation-gates.md +484 -484
- package/.claude/settings.local.json +42 -42
- package/.claude/templates/context-template.md +45 -45
- package/.claude/templates/flags-template.json +42 -42
- package/.claude/templates/phases-sections/accessibility-test.md +17 -17
- package/.claude/templates/phases-sections/api-design.md +37 -37
- package/.claude/templates/phases-sections/backend-tests.md +16 -16
- package/.claude/templates/phases-sections/backend.md +37 -37
- package/.claude/templates/phases-sections/business-logic-validation.md +16 -16
- package/.claude/templates/phases-sections/component-tests.md +17 -17
- package/.claude/templates/phases-sections/contract-backend.md +16 -16
- package/.claude/templates/phases-sections/contract-frontend.md +16 -16
- package/.claude/templates/phases-sections/database.md +35 -35
- package/.claude/templates/phases-sections/e2e-tests.md +16 -16
- package/.claude/templates/phases-sections/fix-implementation.md +17 -17
- package/.claude/templates/phases-sections/frontend-integration.md +18 -18
- package/.claude/templates/phases-sections/manual-flow-test.md +15 -15
- package/.claude/templates/phases-sections/manual-ux-test.md +16 -16
- package/.claude/templates/phases-sections/refactor-implementation.md +17 -17
- package/.claude/templates/phases-sections/refactor.md +16 -16
- package/.claude/templates/phases-sections/regression-tests.md +15 -15
- package/.claude/templates/phases-sections/responsive-test.md +16 -16
- package/.claude/templates/phases-sections/script-implementation.md +43 -43
- package/.claude/templates/phases-sections/test-coverage.md +16 -16
- package/.claude/templates/phases-sections/user-approval.md +14 -14
- package/LICENSE +21 -21
- package/README.md +25 -0
- package/package.json +8 -4
|
@@ -1,478 +1,478 @@
|
|
|
1
|
-
# Error Handling & Resilience Patterns
|
|
2
|
-
|
|
3
|
-
**Core Principles:**
|
|
4
|
-
1. **Fail Fast** - Detect errors early and raise exceptions immediately
|
|
5
|
-
2. **Log Everything** - Every error must be logged with context
|
|
6
|
-
3. **User-Friendly Messages** - Never expose technical details to users
|
|
7
|
-
4. **Graceful Degradation** - System should degrade gracefully, not crash
|
|
8
|
-
5. **Retry with Backoff** - Transient errors should be retried intelligently
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## API Error Boundaries
|
|
13
|
-
|
|
14
|
-
### Next.js API Route Pattern
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
// app/api/items/route.ts
|
|
18
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
19
|
-
import { z } from 'zod'
|
|
20
|
-
import { Prisma } from '@prisma/client'
|
|
21
|
-
import { logger } from '@/lib/logger'
|
|
22
|
-
|
|
23
|
-
export async function POST(request: NextRequest) {
|
|
24
|
-
const requestId = crypto.randomUUID()
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// 1. Parse and validate input
|
|
28
|
-
const body = await request.json()
|
|
29
|
-
const validated = schema.parse(body)
|
|
30
|
-
|
|
31
|
-
// 2. Business logic
|
|
32
|
-
const result = await createItem(validated)
|
|
33
|
-
|
|
34
|
-
// 3. Log success
|
|
35
|
-
logger.info('api_success', { requestId, route: '/api/items' })
|
|
36
|
-
|
|
37
|
-
return NextResponse.json(result)
|
|
38
|
-
|
|
39
|
-
} catch (error) {
|
|
40
|
-
logger.error('api_error', {
|
|
41
|
-
requestId,
|
|
42
|
-
route: '/api/items',
|
|
43
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
44
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
return handleError(error, requestId)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function handleError(error: unknown, requestId: string): NextResponse {
|
|
52
|
-
// 1. Zod validation errors
|
|
53
|
-
if (error instanceof z.ZodError) {
|
|
54
|
-
return NextResponse.json(
|
|
55
|
-
{
|
|
56
|
-
error: 'Validation failed',
|
|
57
|
-
details: error.flatten().fieldErrors,
|
|
58
|
-
requestId
|
|
59
|
-
},
|
|
60
|
-
{ status: 400 }
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 2. Prisma database errors
|
|
65
|
-
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
66
|
-
// P2002: Unique constraint violation
|
|
67
|
-
if (error.code === 'P2002') {
|
|
68
|
-
return NextResponse.json(
|
|
69
|
-
{
|
|
70
|
-
error: 'Resource already exists',
|
|
71
|
-
field: error.meta?.target,
|
|
72
|
-
requestId
|
|
73
|
-
},
|
|
74
|
-
{ status: 409 }
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// P2025: Record not found
|
|
79
|
-
if (error.code === 'P2025') {
|
|
80
|
-
return NextResponse.json(
|
|
81
|
-
{ error: 'Resource not found', requestId },
|
|
82
|
-
{ status: 404 }
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return NextResponse.json(
|
|
87
|
-
{ error: 'Database error', requestId },
|
|
88
|
-
{ status: 500 }
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 3. Custom application errors
|
|
93
|
-
if (error instanceof AppError) {
|
|
94
|
-
return NextResponse.json(
|
|
95
|
-
{ error: error.message, code: error.code, requestId },
|
|
96
|
-
{ status: error.statusCode }
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 4. Unknown errors
|
|
101
|
-
return NextResponse.json(
|
|
102
|
-
{
|
|
103
|
-
error: 'Internal server error',
|
|
104
|
-
message: process.env.NODE_ENV === 'development'
|
|
105
|
-
? (error as Error).message
|
|
106
|
-
: undefined,
|
|
107
|
-
requestId
|
|
108
|
-
},
|
|
109
|
-
{ status: 500 }
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
### FastAPI Error Handler
|
|
117
|
-
|
|
118
|
-
```python
|
|
119
|
-
# app/api/items.py
|
|
120
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
121
|
-
from pydantic import BaseModel, ValidationError
|
|
122
|
-
from sqlalchemy.exc import IntegrityError
|
|
123
|
-
import logging
|
|
124
|
-
|
|
125
|
-
router = APIRouter()
|
|
126
|
-
logger = logging.getLogger(__name__)
|
|
127
|
-
|
|
128
|
-
class CreateItemRequest(BaseModel):
|
|
129
|
-
name: str
|
|
130
|
-
price: float
|
|
131
|
-
|
|
132
|
-
@router.post("/items")
|
|
133
|
-
async def create_item(request: CreateItemRequest):
|
|
134
|
-
request_id = str(uuid.uuid4())
|
|
135
|
-
|
|
136
|
-
try:
|
|
137
|
-
# Business logic
|
|
138
|
-
item = await db.create_item(request.name, request.price)
|
|
139
|
-
|
|
140
|
-
logger.info({"event": "api_success", "request_id": request_id})
|
|
141
|
-
|
|
142
|
-
return {"id": item.id, "name": item.name, "price": item.price}
|
|
143
|
-
|
|
144
|
-
except ValidationError as e:
|
|
145
|
-
logger.warning({"event": "validation_error", "request_id": request_id, "errors": e.errors()})
|
|
146
|
-
raise HTTPException(status_code=400, detail={"error": "Validation failed", "details": e.errors()})
|
|
147
|
-
|
|
148
|
-
except IntegrityError as e:
|
|
149
|
-
logger.error({"event": "db_integrity_error", "request_id": request_id})
|
|
150
|
-
raise HTTPException(status_code=409, detail={"error": "Resource already exists"})
|
|
151
|
-
|
|
152
|
-
except Exception as e:
|
|
153
|
-
logger.error({"event": "api_error", "request_id": request_id, "error": str(e)})
|
|
154
|
-
raise HTTPException(status_code=500, detail={"error": "Internal server error", "request_id": request_id})
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## Custom Error Classes
|
|
160
|
-
|
|
161
|
-
### TypeScript
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// lib/errors.ts
|
|
165
|
-
|
|
166
|
-
export class AppError extends Error {
|
|
167
|
-
constructor(
|
|
168
|
-
public message: string,
|
|
169
|
-
public statusCode: number,
|
|
170
|
-
public code?: string
|
|
171
|
-
) {
|
|
172
|
-
super(message)
|
|
173
|
-
this.name = 'AppError'
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export class ValidationError extends AppError {
|
|
178
|
-
constructor(message: string, public field?: string) {
|
|
179
|
-
super(message, 400, 'VALIDATION_ERROR')
|
|
180
|
-
this.name = 'ValidationError'
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export class NotFoundError extends AppError {
|
|
185
|
-
constructor(resource: string) {
|
|
186
|
-
super(`${resource} not found`, 404, 'NOT_FOUND')
|
|
187
|
-
this.name = 'NotFoundError'
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export class UnauthorizedError extends AppError {
|
|
192
|
-
constructor(message = 'Unauthorized') {
|
|
193
|
-
super(message, 401, 'UNAUTHORIZED')
|
|
194
|
-
this.name = 'UnauthorizedError'
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export class ExternalServiceError extends AppError {
|
|
199
|
-
constructor(
|
|
200
|
-
service: string,
|
|
201
|
-
public originalError?: Error
|
|
202
|
-
) {
|
|
203
|
-
super(`${service} service error`, 503, 'EXTERNAL_SERVICE_ERROR')
|
|
204
|
-
this.name = 'ExternalServiceError'
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Usage
|
|
209
|
-
throw new NotFoundError('User')
|
|
210
|
-
throw new ValidationError('Invalid email format', 'email')
|
|
211
|
-
throw new ExternalServiceError('PaymentGateway', originalError)
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Python
|
|
215
|
-
|
|
216
|
-
```python
|
|
217
|
-
# lib/errors.py
|
|
218
|
-
|
|
219
|
-
class AppError(Exception):
|
|
220
|
-
def __init__(self, message: str, status_code: int, code: str = None):
|
|
221
|
-
self.message = message
|
|
222
|
-
self.status_code = status_code
|
|
223
|
-
self.code = code
|
|
224
|
-
super().__init__(self.message)
|
|
225
|
-
|
|
226
|
-
class ValidationError(AppError):
|
|
227
|
-
def __init__(self, message: str, field: str = None):
|
|
228
|
-
super().__init__(message, 400, "VALIDATION_ERROR")
|
|
229
|
-
self.field = field
|
|
230
|
-
|
|
231
|
-
class NotFoundError(AppError):
|
|
232
|
-
def __init__(self, resource: str):
|
|
233
|
-
super().__init__(f"{resource} not found", 404, "NOT_FOUND")
|
|
234
|
-
|
|
235
|
-
class UnauthorizedError(AppError):
|
|
236
|
-
def __init__(self, message: str = "Unauthorized"):
|
|
237
|
-
super().__init__(message, 401, "UNAUTHORIZED")
|
|
238
|
-
|
|
239
|
-
class ExternalServiceError(AppError):
|
|
240
|
-
def __init__(self, service: str, original_error: Exception = None):
|
|
241
|
-
super().__init__(f"{service} service error", 503, "EXTERNAL_SERVICE_ERROR")
|
|
242
|
-
self.original_error = original_error
|
|
243
|
-
|
|
244
|
-
# Usage
|
|
245
|
-
raise NotFoundError("User")
|
|
246
|
-
raise ValidationError("Invalid email format", field="email")
|
|
247
|
-
raise ExternalServiceError("PaymentGateway", original_error)
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## Retry Logic with Exponential Backoff
|
|
253
|
-
|
|
254
|
-
```typescript
|
|
255
|
-
// lib/retry.ts
|
|
256
|
-
import { logger } from '@/lib/logger'
|
|
257
|
-
|
|
258
|
-
interface RetryOptions {
|
|
259
|
-
maxRetries?: number
|
|
260
|
-
initialDelayMs?: number
|
|
261
|
-
maxDelayMs?: number
|
|
262
|
-
exponentialBase?: number
|
|
263
|
-
shouldRetry?: (error: Error) => boolean
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export async function withRetry<T>(
|
|
267
|
-
fn: () => Promise<T>,
|
|
268
|
-
options: RetryOptions = {}
|
|
269
|
-
): Promise<T> {
|
|
270
|
-
const {
|
|
271
|
-
maxRetries = 3,
|
|
272
|
-
initialDelayMs = 1000,
|
|
273
|
-
maxDelayMs = 10000,
|
|
274
|
-
exponentialBase = 2,
|
|
275
|
-
shouldRetry = () => true
|
|
276
|
-
} = options
|
|
277
|
-
|
|
278
|
-
let lastError: Error
|
|
279
|
-
|
|
280
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
281
|
-
try {
|
|
282
|
-
logger.info('retry_attempt', { attempt, maxRetries })
|
|
283
|
-
return await fn()
|
|
284
|
-
|
|
285
|
-
} catch (error) {
|
|
286
|
-
lastError = error as Error
|
|
287
|
-
|
|
288
|
-
if (!shouldRetry(lastError)) {
|
|
289
|
-
logger.error('retry_non_retryable', { attempt, error: lastError.message })
|
|
290
|
-
throw lastError
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (attempt === maxRetries) {
|
|
294
|
-
logger.error('retry_exhausted', { attempt, maxRetries, error: lastError.message })
|
|
295
|
-
throw lastError
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const delay = Math.min(
|
|
299
|
-
initialDelayMs * Math.pow(exponentialBase, attempt - 1),
|
|
300
|
-
maxDelayMs
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
logger.warn('retry_waiting', {
|
|
304
|
-
attempt,
|
|
305
|
-
nextAttempt: attempt + 1,
|
|
306
|
-
delayMs: delay,
|
|
307
|
-
error: lastError.message
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
await new Promise(resolve => setTimeout(resolve, delay))
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
throw lastError!
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Usage - External API with retry
|
|
318
|
-
export async function fetchExternalDataWithRetry(userId: string) {
|
|
319
|
-
return withRetry(
|
|
320
|
-
() => fetchExternalData(userId),
|
|
321
|
-
{
|
|
322
|
-
maxRetries: 3,
|
|
323
|
-
initialDelayMs: 1000,
|
|
324
|
-
shouldRetry: (error) => {
|
|
325
|
-
// Retry on network errors or 5xx status codes
|
|
326
|
-
return (
|
|
327
|
-
error.message.includes('network') ||
|
|
328
|
-
error.message.includes('timeout') ||
|
|
329
|
-
error.message.includes('503')
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
---
|
|
338
|
-
|
|
339
|
-
## Circuit Breaker Pattern
|
|
340
|
-
|
|
341
|
-
For external services that may go down.
|
|
342
|
-
|
|
343
|
-
```typescript
|
|
344
|
-
// lib/circuit-breaker.ts
|
|
345
|
-
import { logger } from '@/lib/logger'
|
|
346
|
-
|
|
347
|
-
export class CircuitBreaker {
|
|
348
|
-
private failures = 0
|
|
349
|
-
private lastFailureTime: number | null = null
|
|
350
|
-
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'
|
|
351
|
-
|
|
352
|
-
constructor(
|
|
353
|
-
private threshold: number = 5, // Open after 5 failures
|
|
354
|
-
private timeout: number = 60000, // Reset after 1 minute
|
|
355
|
-
private halfOpenRequests: number = 1 // Test with 1 request
|
|
356
|
-
) {}
|
|
357
|
-
|
|
358
|
-
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
359
|
-
if (this.state === 'OPEN') {
|
|
360
|
-
// Check if we should try again (timeout elapsed)
|
|
361
|
-
if (Date.now() - this.lastFailureTime! >= this.timeout) {
|
|
362
|
-
logger.info('circuit_breaker_half_open')
|
|
363
|
-
this.state = 'HALF_OPEN'
|
|
364
|
-
} else {
|
|
365
|
-
logger.warn('circuit_breaker_rejected')
|
|
366
|
-
throw new Error('Circuit breaker is OPEN')
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
const result = await fn()
|
|
372
|
-
|
|
373
|
-
// Success - reset failures
|
|
374
|
-
if (this.state === 'HALF_OPEN') {
|
|
375
|
-
logger.info('circuit_breaker_closed')
|
|
376
|
-
this.state = 'CLOSED'
|
|
377
|
-
this.failures = 0
|
|
378
|
-
this.lastFailureTime = null
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return result
|
|
382
|
-
|
|
383
|
-
} catch (error) {
|
|
384
|
-
this.failures++
|
|
385
|
-
this.lastFailureTime = Date.now()
|
|
386
|
-
|
|
387
|
-
if (this.failures >= this.threshold) {
|
|
388
|
-
logger.error('circuit_breaker_opened', {
|
|
389
|
-
failures: this.failures,
|
|
390
|
-
threshold: this.threshold
|
|
391
|
-
})
|
|
392
|
-
this.state = 'OPEN'
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
throw error
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
getState() {
|
|
400
|
-
return this.state
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Usage
|
|
405
|
-
const externalApiCircuit = new CircuitBreaker(5, 60000)
|
|
406
|
-
|
|
407
|
-
export async function fetchDataSafe(userId: string) {
|
|
408
|
-
return externalApiCircuit.execute(() =>
|
|
409
|
-
fetchExternalData(userId)
|
|
410
|
-
)
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
---
|
|
415
|
-
|
|
416
|
-
## Input Validation (Zod)
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
// Always validate with Zod
|
|
420
|
-
import { z } from 'zod'
|
|
421
|
-
|
|
422
|
-
const userSchema = z.object({
|
|
423
|
-
email: z.string().email(),
|
|
424
|
-
age: z.number().min(18).max(120),
|
|
425
|
-
country: z.string().length(2), // ISO country code
|
|
426
|
-
newsletter: z.boolean().optional()
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
export async function POST(request: NextRequest) {
|
|
430
|
-
const body = await request.json()
|
|
431
|
-
|
|
432
|
-
// Safe parse (returns result object)
|
|
433
|
-
const result = userSchema.safeParse(body)
|
|
434
|
-
|
|
435
|
-
if (!result.success) {
|
|
436
|
-
logger.warn('validation_error', {
|
|
437
|
-
errors: result.error.flatten()
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
return NextResponse.json(
|
|
441
|
-
{
|
|
442
|
-
error: 'Invalid input',
|
|
443
|
-
details: result.error.flatten().fieldErrors
|
|
444
|
-
},
|
|
445
|
-
{ status: 400 }
|
|
446
|
-
)
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Use validated data
|
|
450
|
-
const data = result.data // Type-safe!
|
|
451
|
-
// ...
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
|
-
## Best Practices
|
|
458
|
-
|
|
459
|
-
### DO:
|
|
460
|
-
- ✅ Validate all inputs (never trust user data)
|
|
461
|
-
- ✅ Log every error with context (requestId, userId, etc.)
|
|
462
|
-
- ✅ Return user-friendly error messages
|
|
463
|
-
- ✅ Use custom error classes for domain errors
|
|
464
|
-
- ✅ Implement retry logic for transient failures
|
|
465
|
-
- ✅ Use circuit breakers for external services
|
|
466
|
-
- ✅ Fail fast (don't continue with invalid data)
|
|
467
|
-
|
|
468
|
-
### DON'T:
|
|
469
|
-
- ❌ Expose stack traces to users
|
|
470
|
-
- ❌ Return raw database errors
|
|
471
|
-
- ❌ Catch errors and do nothing
|
|
472
|
-
- ❌ Use generic error messages ("Something went wrong")
|
|
473
|
-
- ❌ Retry non-retryable errors (validation, 404, etc.)
|
|
474
|
-
- ❌ Hardcode API keys in error logs
|
|
475
|
-
|
|
476
|
-
---
|
|
477
|
-
|
|
478
|
-
**💡 Remember:** Good error handling makes debugging 10x easier!
|
|
1
|
+
# Error Handling & Resilience Patterns
|
|
2
|
+
|
|
3
|
+
**Core Principles:**
|
|
4
|
+
1. **Fail Fast** - Detect errors early and raise exceptions immediately
|
|
5
|
+
2. **Log Everything** - Every error must be logged with context
|
|
6
|
+
3. **User-Friendly Messages** - Never expose technical details to users
|
|
7
|
+
4. **Graceful Degradation** - System should degrade gracefully, not crash
|
|
8
|
+
5. **Retry with Backoff** - Transient errors should be retried intelligently
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## API Error Boundaries
|
|
13
|
+
|
|
14
|
+
### Next.js API Route Pattern
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// app/api/items/route.ts
|
|
18
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
19
|
+
import { z } from 'zod'
|
|
20
|
+
import { Prisma } from '@prisma/client'
|
|
21
|
+
import { logger } from '@/lib/logger'
|
|
22
|
+
|
|
23
|
+
export async function POST(request: NextRequest) {
|
|
24
|
+
const requestId = crypto.randomUUID()
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// 1. Parse and validate input
|
|
28
|
+
const body = await request.json()
|
|
29
|
+
const validated = schema.parse(body)
|
|
30
|
+
|
|
31
|
+
// 2. Business logic
|
|
32
|
+
const result = await createItem(validated)
|
|
33
|
+
|
|
34
|
+
// 3. Log success
|
|
35
|
+
logger.info('api_success', { requestId, route: '/api/items' })
|
|
36
|
+
|
|
37
|
+
return NextResponse.json(result)
|
|
38
|
+
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.error('api_error', {
|
|
41
|
+
requestId,
|
|
42
|
+
route: '/api/items',
|
|
43
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
44
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return handleError(error, requestId)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleError(error: unknown, requestId: string): NextResponse {
|
|
52
|
+
// 1. Zod validation errors
|
|
53
|
+
if (error instanceof z.ZodError) {
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{
|
|
56
|
+
error: 'Validation failed',
|
|
57
|
+
details: error.flatten().fieldErrors,
|
|
58
|
+
requestId
|
|
59
|
+
},
|
|
60
|
+
{ status: 400 }
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Prisma database errors
|
|
65
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
66
|
+
// P2002: Unique constraint violation
|
|
67
|
+
if (error.code === 'P2002') {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{
|
|
70
|
+
error: 'Resource already exists',
|
|
71
|
+
field: error.meta?.target,
|
|
72
|
+
requestId
|
|
73
|
+
},
|
|
74
|
+
{ status: 409 }
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// P2025: Record not found
|
|
79
|
+
if (error.code === 'P2025') {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: 'Resource not found', requestId },
|
|
82
|
+
{ status: 404 }
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: 'Database error', requestId },
|
|
88
|
+
{ status: 500 }
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Custom application errors
|
|
93
|
+
if (error instanceof AppError) {
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: error.message, code: error.code, requestId },
|
|
96
|
+
{ status: error.statusCode }
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Unknown errors
|
|
101
|
+
return NextResponse.json(
|
|
102
|
+
{
|
|
103
|
+
error: 'Internal server error',
|
|
104
|
+
message: process.env.NODE_ENV === 'development'
|
|
105
|
+
? (error as Error).message
|
|
106
|
+
: undefined,
|
|
107
|
+
requestId
|
|
108
|
+
},
|
|
109
|
+
{ status: 500 }
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### FastAPI Error Handler
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# app/api/items.py
|
|
120
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
121
|
+
from pydantic import BaseModel, ValidationError
|
|
122
|
+
from sqlalchemy.exc import IntegrityError
|
|
123
|
+
import logging
|
|
124
|
+
|
|
125
|
+
router = APIRouter()
|
|
126
|
+
logger = logging.getLogger(__name__)
|
|
127
|
+
|
|
128
|
+
class CreateItemRequest(BaseModel):
|
|
129
|
+
name: str
|
|
130
|
+
price: float
|
|
131
|
+
|
|
132
|
+
@router.post("/items")
|
|
133
|
+
async def create_item(request: CreateItemRequest):
|
|
134
|
+
request_id = str(uuid.uuid4())
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Business logic
|
|
138
|
+
item = await db.create_item(request.name, request.price)
|
|
139
|
+
|
|
140
|
+
logger.info({"event": "api_success", "request_id": request_id})
|
|
141
|
+
|
|
142
|
+
return {"id": item.id, "name": item.name, "price": item.price}
|
|
143
|
+
|
|
144
|
+
except ValidationError as e:
|
|
145
|
+
logger.warning({"event": "validation_error", "request_id": request_id, "errors": e.errors()})
|
|
146
|
+
raise HTTPException(status_code=400, detail={"error": "Validation failed", "details": e.errors()})
|
|
147
|
+
|
|
148
|
+
except IntegrityError as e:
|
|
149
|
+
logger.error({"event": "db_integrity_error", "request_id": request_id})
|
|
150
|
+
raise HTTPException(status_code=409, detail={"error": "Resource already exists"})
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error({"event": "api_error", "request_id": request_id, "error": str(e)})
|
|
154
|
+
raise HTTPException(status_code=500, detail={"error": "Internal server error", "request_id": request_id})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Custom Error Classes
|
|
160
|
+
|
|
161
|
+
### TypeScript
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// lib/errors.ts
|
|
165
|
+
|
|
166
|
+
export class AppError extends Error {
|
|
167
|
+
constructor(
|
|
168
|
+
public message: string,
|
|
169
|
+
public statusCode: number,
|
|
170
|
+
public code?: string
|
|
171
|
+
) {
|
|
172
|
+
super(message)
|
|
173
|
+
this.name = 'AppError'
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class ValidationError extends AppError {
|
|
178
|
+
constructor(message: string, public field?: string) {
|
|
179
|
+
super(message, 400, 'VALIDATION_ERROR')
|
|
180
|
+
this.name = 'ValidationError'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export class NotFoundError extends AppError {
|
|
185
|
+
constructor(resource: string) {
|
|
186
|
+
super(`${resource} not found`, 404, 'NOT_FOUND')
|
|
187
|
+
this.name = 'NotFoundError'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export class UnauthorizedError extends AppError {
|
|
192
|
+
constructor(message = 'Unauthorized') {
|
|
193
|
+
super(message, 401, 'UNAUTHORIZED')
|
|
194
|
+
this.name = 'UnauthorizedError'
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export class ExternalServiceError extends AppError {
|
|
199
|
+
constructor(
|
|
200
|
+
service: string,
|
|
201
|
+
public originalError?: Error
|
|
202
|
+
) {
|
|
203
|
+
super(`${service} service error`, 503, 'EXTERNAL_SERVICE_ERROR')
|
|
204
|
+
this.name = 'ExternalServiceError'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Usage
|
|
209
|
+
throw new NotFoundError('User')
|
|
210
|
+
throw new ValidationError('Invalid email format', 'email')
|
|
211
|
+
throw new ExternalServiceError('PaymentGateway', originalError)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Python
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# lib/errors.py
|
|
218
|
+
|
|
219
|
+
class AppError(Exception):
|
|
220
|
+
def __init__(self, message: str, status_code: int, code: str = None):
|
|
221
|
+
self.message = message
|
|
222
|
+
self.status_code = status_code
|
|
223
|
+
self.code = code
|
|
224
|
+
super().__init__(self.message)
|
|
225
|
+
|
|
226
|
+
class ValidationError(AppError):
|
|
227
|
+
def __init__(self, message: str, field: str = None):
|
|
228
|
+
super().__init__(message, 400, "VALIDATION_ERROR")
|
|
229
|
+
self.field = field
|
|
230
|
+
|
|
231
|
+
class NotFoundError(AppError):
|
|
232
|
+
def __init__(self, resource: str):
|
|
233
|
+
super().__init__(f"{resource} not found", 404, "NOT_FOUND")
|
|
234
|
+
|
|
235
|
+
class UnauthorizedError(AppError):
|
|
236
|
+
def __init__(self, message: str = "Unauthorized"):
|
|
237
|
+
super().__init__(message, 401, "UNAUTHORIZED")
|
|
238
|
+
|
|
239
|
+
class ExternalServiceError(AppError):
|
|
240
|
+
def __init__(self, service: str, original_error: Exception = None):
|
|
241
|
+
super().__init__(f"{service} service error", 503, "EXTERNAL_SERVICE_ERROR")
|
|
242
|
+
self.original_error = original_error
|
|
243
|
+
|
|
244
|
+
# Usage
|
|
245
|
+
raise NotFoundError("User")
|
|
246
|
+
raise ValidationError("Invalid email format", field="email")
|
|
247
|
+
raise ExternalServiceError("PaymentGateway", original_error)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Retry Logic with Exponential Backoff
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// lib/retry.ts
|
|
256
|
+
import { logger } from '@/lib/logger'
|
|
257
|
+
|
|
258
|
+
interface RetryOptions {
|
|
259
|
+
maxRetries?: number
|
|
260
|
+
initialDelayMs?: number
|
|
261
|
+
maxDelayMs?: number
|
|
262
|
+
exponentialBase?: number
|
|
263
|
+
shouldRetry?: (error: Error) => boolean
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function withRetry<T>(
|
|
267
|
+
fn: () => Promise<T>,
|
|
268
|
+
options: RetryOptions = {}
|
|
269
|
+
): Promise<T> {
|
|
270
|
+
const {
|
|
271
|
+
maxRetries = 3,
|
|
272
|
+
initialDelayMs = 1000,
|
|
273
|
+
maxDelayMs = 10000,
|
|
274
|
+
exponentialBase = 2,
|
|
275
|
+
shouldRetry = () => true
|
|
276
|
+
} = options
|
|
277
|
+
|
|
278
|
+
let lastError: Error
|
|
279
|
+
|
|
280
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
281
|
+
try {
|
|
282
|
+
logger.info('retry_attempt', { attempt, maxRetries })
|
|
283
|
+
return await fn()
|
|
284
|
+
|
|
285
|
+
} catch (error) {
|
|
286
|
+
lastError = error as Error
|
|
287
|
+
|
|
288
|
+
if (!shouldRetry(lastError)) {
|
|
289
|
+
logger.error('retry_non_retryable', { attempt, error: lastError.message })
|
|
290
|
+
throw lastError
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (attempt === maxRetries) {
|
|
294
|
+
logger.error('retry_exhausted', { attempt, maxRetries, error: lastError.message })
|
|
295
|
+
throw lastError
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const delay = Math.min(
|
|
299
|
+
initialDelayMs * Math.pow(exponentialBase, attempt - 1),
|
|
300
|
+
maxDelayMs
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
logger.warn('retry_waiting', {
|
|
304
|
+
attempt,
|
|
305
|
+
nextAttempt: attempt + 1,
|
|
306
|
+
delayMs: delay,
|
|
307
|
+
error: lastError.message
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw lastError!
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Usage - External API with retry
|
|
318
|
+
export async function fetchExternalDataWithRetry(userId: string) {
|
|
319
|
+
return withRetry(
|
|
320
|
+
() => fetchExternalData(userId),
|
|
321
|
+
{
|
|
322
|
+
maxRetries: 3,
|
|
323
|
+
initialDelayMs: 1000,
|
|
324
|
+
shouldRetry: (error) => {
|
|
325
|
+
// Retry on network errors or 5xx status codes
|
|
326
|
+
return (
|
|
327
|
+
error.message.includes('network') ||
|
|
328
|
+
error.message.includes('timeout') ||
|
|
329
|
+
error.message.includes('503')
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Circuit Breaker Pattern
|
|
340
|
+
|
|
341
|
+
For external services that may go down.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// lib/circuit-breaker.ts
|
|
345
|
+
import { logger } from '@/lib/logger'
|
|
346
|
+
|
|
347
|
+
export class CircuitBreaker {
|
|
348
|
+
private failures = 0
|
|
349
|
+
private lastFailureTime: number | null = null
|
|
350
|
+
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'
|
|
351
|
+
|
|
352
|
+
constructor(
|
|
353
|
+
private threshold: number = 5, // Open after 5 failures
|
|
354
|
+
private timeout: number = 60000, // Reset after 1 minute
|
|
355
|
+
private halfOpenRequests: number = 1 // Test with 1 request
|
|
356
|
+
) {}
|
|
357
|
+
|
|
358
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
359
|
+
if (this.state === 'OPEN') {
|
|
360
|
+
// Check if we should try again (timeout elapsed)
|
|
361
|
+
if (Date.now() - this.lastFailureTime! >= this.timeout) {
|
|
362
|
+
logger.info('circuit_breaker_half_open')
|
|
363
|
+
this.state = 'HALF_OPEN'
|
|
364
|
+
} else {
|
|
365
|
+
logger.warn('circuit_breaker_rejected')
|
|
366
|
+
throw new Error('Circuit breaker is OPEN')
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const result = await fn()
|
|
372
|
+
|
|
373
|
+
// Success - reset failures
|
|
374
|
+
if (this.state === 'HALF_OPEN') {
|
|
375
|
+
logger.info('circuit_breaker_closed')
|
|
376
|
+
this.state = 'CLOSED'
|
|
377
|
+
this.failures = 0
|
|
378
|
+
this.lastFailureTime = null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.failures++
|
|
385
|
+
this.lastFailureTime = Date.now()
|
|
386
|
+
|
|
387
|
+
if (this.failures >= this.threshold) {
|
|
388
|
+
logger.error('circuit_breaker_opened', {
|
|
389
|
+
failures: this.failures,
|
|
390
|
+
threshold: this.threshold
|
|
391
|
+
})
|
|
392
|
+
this.state = 'OPEN'
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
throw error
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
getState() {
|
|
400
|
+
return this.state
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Usage
|
|
405
|
+
const externalApiCircuit = new CircuitBreaker(5, 60000)
|
|
406
|
+
|
|
407
|
+
export async function fetchDataSafe(userId: string) {
|
|
408
|
+
return externalApiCircuit.execute(() =>
|
|
409
|
+
fetchExternalData(userId)
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Input Validation (Zod)
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// Always validate with Zod
|
|
420
|
+
import { z } from 'zod'
|
|
421
|
+
|
|
422
|
+
const userSchema = z.object({
|
|
423
|
+
email: z.string().email(),
|
|
424
|
+
age: z.number().min(18).max(120),
|
|
425
|
+
country: z.string().length(2), // ISO country code
|
|
426
|
+
newsletter: z.boolean().optional()
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
export async function POST(request: NextRequest) {
|
|
430
|
+
const body = await request.json()
|
|
431
|
+
|
|
432
|
+
// Safe parse (returns result object)
|
|
433
|
+
const result = userSchema.safeParse(body)
|
|
434
|
+
|
|
435
|
+
if (!result.success) {
|
|
436
|
+
logger.warn('validation_error', {
|
|
437
|
+
errors: result.error.flatten()
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
return NextResponse.json(
|
|
441
|
+
{
|
|
442
|
+
error: 'Invalid input',
|
|
443
|
+
details: result.error.flatten().fieldErrors
|
|
444
|
+
},
|
|
445
|
+
{ status: 400 }
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Use validated data
|
|
450
|
+
const data = result.data // Type-safe!
|
|
451
|
+
// ...
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Best Practices
|
|
458
|
+
|
|
459
|
+
### DO:
|
|
460
|
+
- ✅ Validate all inputs (never trust user data)
|
|
461
|
+
- ✅ Log every error with context (requestId, userId, etc.)
|
|
462
|
+
- ✅ Return user-friendly error messages
|
|
463
|
+
- ✅ Use custom error classes for domain errors
|
|
464
|
+
- ✅ Implement retry logic for transient failures
|
|
465
|
+
- ✅ Use circuit breakers for external services
|
|
466
|
+
- ✅ Fail fast (don't continue with invalid data)
|
|
467
|
+
|
|
468
|
+
### DON'T:
|
|
469
|
+
- ❌ Expose stack traces to users
|
|
470
|
+
- ❌ Return raw database errors
|
|
471
|
+
- ❌ Catch errors and do nothing
|
|
472
|
+
- ❌ Use generic error messages ("Something went wrong")
|
|
473
|
+
- ❌ Retry non-retryable errors (validation, 404, etc.)
|
|
474
|
+
- ❌ Hardcode API keys in error logs
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
**💡 Remember:** Good error handling makes debugging 10x easier!
|