# 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`.
