# 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: ```json { "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 `BaseError`s) 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`.