40227-vm/backend/docs/error-handling.md
2026-06-10 18:27:19 +02:00

2.8 KiB

Backend Error Handling

Backend errors use a single centralized path, mirroring the frontend (frontend/docs/error-handling.md). The goal: one place decides the HTTP status, the response body, and what gets logged — handlers and services only throw.

Pipeline

  1. Route handlers are wrapped in Helpers.wrapAsync, whose .catch(next) forwards any thrown/rejected error to Express.
  2. With no per-router error middleware, the error bubbles to the single terminal handler registered last in src/index.ts: errorHandler from src/middlewares/error-handler.ts.
  3. errorHandler calls the pure normalizeError(error) to get { status, body, unexpected }, logs unexpected (5xx) errors through the central logger, and sends body as JSON with status.
  4. Requests matching no route hit notFoundHandler (mounted on /api), which forwards a NotFoundError into the same errorHandler.

Response shape

The JSON body is exactly what the frontend ApiError consumes:

{ "message": "human-readable text", "code": "machine.readable.key", "details": null }

code and details are omitted when absent. The HTTP status carries 4xx/5xx; the frontend treats 401/403 as session expiry (AuthExpiredError).

Error classes (src/services/notifications/errors/)

  • AppError(status, message, { code?, details? }) — base for all expected (operational) errors. status >= 500 is treated as unexpected (logged).
  • ValidationError → 400, UnauthorizedError → 401, ForbiddenError → 403, NotFoundError → 404. Each resolves its message from services/notifications/list.ts and carries the notification key as code.

normalizeError also maps Sequelize ValidationError/UniqueConstraintError (and other BaseErrors) to a client 400, and any unknown/native error to a generic 500 whose body never leaks internals (the original is logged).

Rules

  • Throw an AppError subclass for expected failures; never res.status().send() a raw error or hand-format an error body in a handler.
  • Do not add per-router error middleware or scatter console.error/console.log; log through @/services/logger (logger.error/warn/info).
  • Pass native errors from external services through as-is (don't wrap), so the central handler surfaces them; only it decides the client-facing 500 body.
  • Boot/config validation (config.ts), CLI scripts (db/umzug.ts, db/reset.ts) and runtime invariants may still throw native Error — they crash the process or map to a generic 500, which is intended.

Verification

npm test runs src/middlewares/error-handler.test.ts (Node test runner via tsx), covering AppError subclasses, the Sequelize mapping, native errors, and non-Error thrown values. The build (tsconfig.build.json) excludes *.test.ts from dist.