Error Handling Pattern
Centralized error handling across backend, frontend, and the shared API contract.
Backend Error Handler
backend/src/middleware/errorHandler.ts is the centralized error handler in the Middleware Stack:
- Catches all unhandled errors from route handlers and services
- Maps custom
AppErrorsubclasses to HTTP status codes - Handles Zod
ZodErrorseparately (400 with field details) - Generates unique error IDs for tracking (alongside the request ID from the request logger)
- Logs full context (request ID, path, method, client IP)
- Sends to Sentry with sensitive data scrubbing
- Returns safe error messages in production (no stack traces)
Custom Error Classes
Defined in backend/src/lib/errors.ts (as of 2026-06):
| Class | HTTP Status | Use Case |
|---|---|---|
AppError | — | Abstract base class (statusCode, code, details) |
ValidationError | 400 | Input validation failures |
BadRequestError | 400 | Malformed requests |
AuthenticationError | 401 | Authentication failure |
AuthorizationError | 403 | Authorization failure |
NotFoundError | 404 | Entity not found |
ConflictError | 409 | State conflicts (e.g. duplicates) |
UnprocessableEntityError | 422 | Semantically invalid input |
RateLimitError | 429 | Rate limit exceeded |
DatabaseError | 500 | DB operation failures |
ExternalServiceError | 502 | Upstream API failures (5xx retryable) |
Database Error Classification
Drizzle ≥0.44 wraps driver errors in DrizzleQueryError with the original PostgresError as cause — SQLSTATE codes live on the cause, not on the thrown error, so direct error.code checks silently break. Use the helpers in backend/src/lib/db-errors.ts, e.g. isUniqueViolation(error) (SQLSTATE 23505), which walk the cause chain and work with both wrapped and raw driver shapes (PR#2005).
Frontend Error Handling
| Component | File | Purpose |
|---|---|---|
| Error boundary | frontend/src/components/ErrorBoundary.tsx | Catches React render errors |
| Error parser | frontend/src/utils/errorParser.ts | Extracts user-friendly messages from API errors |
| Stale import reloader | Frontend error boundary | Detects post-deploy chunk loading failures, triggers reload |
API Error Contract
Shared types in shared/api-types.ts:
interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: ApiError;
timestamp?: string;
pagination?: { limit: number; offset: number; count: number };
}The isApiError() type guard is available for safe error narrowing.
Timeout Strategy
Request timeout middleware (backend/src/middleware/timeout.ts) enforces a 55-second limit, which is 5 seconds shorter than the 60-second Fly.io load balancer timeout. This ensures the app returns a proper error response before the load balancer kills the connection.
See Also
- Middleware Stack — error handler position in the pipeline
- Backend Middleware — all middleware files
- Sentry — error monitoring integration
- Shared Layer — API error types
- API Layer Pattern — frontend error handling in API calls
- Validation Pattern — validation error details