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

61 lines
2.8 KiB
Markdown

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