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
- Route handlers are wrapped in
Helpers.wrapAsync, whose.catch(next)forwards any thrown/rejected error to Express. - With no per-router error middleware, the error bubbles to the single terminal
handler registered last in
src/index.ts:errorHandlerfromsrc/middlewares/error-handler.ts. errorHandlercalls the purenormalizeError(error)to get{ status, body, unexpected }, logs unexpected (5xx) errors through the centrallogger, and sendsbodyas JSON withstatus.- Requests matching no route hit
notFoundHandler(mounted on/api), which forwards aNotFoundErrorinto the sameerrorHandler.
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 >= 500is treated as unexpected (logged).ValidationError→ 400,UnauthorizedError→ 401,ForbiddenError→ 403,NotFoundError→ 404. Each resolves its message fromservices/notifications/list.tsand carries the notification key ascode.
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
AppErrorsubclass for expected failures; neverres.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 nativeError— 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.