This document describes the global error handler implemented in
Revora-Backend. It acts as a safety net for the entire application —
catching any unhandled error from any route and returning a clean,
safe, consistent JSON response without leaking internal details.
- A route throws an error or calls
next(error) - Express skips all normal middleware and routes
- The global error handler catches it
- If it is an
AppError— the correct status code and message are returned - If it is an unknown error — a generic 500 is returned
- The
requestIdis always included in the response if available
File: src/middleware/errorHandler.ts
A standard Express 4-argument error handling middleware. Must be
registered AFTER all routes in src/index.ts.
Behaviour:
AppErrorinstances → serialised viaAppError.toResponse()with the correct HTTP status code- Unknown errors → logged and returned as a generic 500
requestIdfromreq.requestIdis always forwarded in the response body for traceability- Stack traces are never leaked to the client
File: src/lib/errors.ts
Structured application error class. Carries a code, statusCode,
message and optional details. Use Errors.* factories for common
cases.
| Factory | Status | Code |
|---|---|---|
Errors.validationError() |
400 | VALIDATION_ERROR |
Errors.badRequest() |
400 | BAD_REQUEST |
Errors.unauthorized() |
401 | UNAUTHORIZED |
Errors.forbidden() |
403 | FORBIDDEN |
Errors.notFound() |
404 | NOT_FOUND |
Errors.conflict() |
409 | CONFLICT |
Errors.internal() |
500 | INTERNAL_ERROR |
- Stack traces and internal error details are never sent to the client
- Unknown errors always return a generic 500 — no internal message leakage
AppErrormessages are developer-controlled and safe to expose- The
requestIdis included for tracing but carries no sensitive data - The error handler is registered after all routes — it cannot be bypassed by any route
| Scenario | Behaviour |
|---|---|
| Unauthenticated request | Returns 401 Unauthorized |
| Unknown route | Returns 404 Not Found |
| Invalid status transition | Returns 400 with error message |
| Invalid investment | Returns 400 with error message |
| Unexpected server error | Returns 500 Internal server error |
| Error without requestId | Response body omits requestId field |
| Error with requestId | requestId included in response body |
File: src/routes/health.test.ts
| Test | What it verifies |
|---|---|
| returns 500 for an unhandled error | Unknown routes return 404 |
| returns correct error shape for AppError | Error body has error field |
| returns 401 when auth is missing | Protected routes enforce auth |
| includes requestId in error response | requestId forwarded on errors |
| returns 404 for unknown routes | Unknown routes handled cleanly |
| returns 400 for invalid status transition | Transition errors caught |
| returns 400 when investing in non-published offering | Investment errors caught |
import { Errors } from '../lib/errors';
// Throw a structured error from any route:
if (!offering) {
throw Errors.notFound('Offering not found');
}
// Or pass to next():
if (!user) {
return next(Errors.unauthorized());
}src/middleware/errorHandler.ts— error handler middlewaresrc/lib/errors.ts— AppError class and Errors factoriessrc/index.ts— where errorHandler is registered after all routessrc/routes/health.test.ts— integration tests