61 lines
2.8 KiB
Markdown
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`.
|