Skip to main content

Error Handling

In one line: Every error follows one structured JSON shape with a machine-readable code, a human message, and a requestId for Sentry correlation. Operational (expected) errors are logged at warn; programmer (unexpected) errors are logged at error and alert via Sentry.

Design Principles

  1. Never leak internals — No stack traces, DB column names, or internal IDs in client responses
  2. Structured, predictable responses — Every error follows the same JSON shape
  3. Machine-readable codes — Clients can programmatically handle specific errors
  4. Human-readable messages — Users see meaningful text, not “Something went wrong”
  5. Correlation everywhere — Every error carries a requestId for Sentry cross-referencing
  6. Fail gracefully — Operational errors return proper HTTP codes; programmer errors trigger alerts

Standard Response Shapes

HTTP Status Code Map

Error Classification

Logged at warn level. No Sentry alert.

Error Code Registry

All error codes are centralised in packages/errors/codes.ts — no magic strings:

AppError Class

API Route Pattern

All API routes are wrapped with withErrorHandler:

Business Layer Pattern

Business logic throws AppError — never catches it. The handler layer above decides what to do:

Request ID Correlation

Every request gets a unique ID attached in middleware:
This ID appears in every error response, every Sentry event, and every log line — making it trivial to trace a specific request across all systems.

Sentry Integration

Rate Limiting

All rate limiting runs in Next.js middleware via @upstash/ratelimit backed by Upstash Redis. Applied before auth checks or any business logic.

Client-Side Error Handling

The client switches on the error code to decide what the user sees: React Error Boundaries catch rendering errors at the route level. Each route group has its own error.tsx that reports to Sentry and shows a retry button.
Last modified on June 29, 2026