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 AppError subclasses to HTTP status codes
  • Handles Zod ZodError separately (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):

ClassHTTP StatusUse Case
AppErrorAbstract base class (statusCode, code, details)
ValidationError400Input validation failures
BadRequestError400Malformed requests
AuthenticationError401Authentication failure
AuthorizationError403Authorization failure
NotFoundError404Entity not found
ConflictError409State conflicts (e.g. duplicates)
UnprocessableEntityError422Semantically invalid input
RateLimitError429Rate limit exceeded
DatabaseError500DB operation failures
ExternalServiceError502Upstream 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

ComponentFilePurpose
Error boundaryfrontend/src/components/ErrorBoundary.tsxCatches React render errors
Error parserfrontend/src/utils/errorParser.tsExtracts user-friendly messages from API errors
Stale import reloaderFrontend error boundaryDetects 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