Hierarchy
PyqDeck uses a custom error hierarchy based on aBaseError class to ensure all errors have a consistent structure and status code.
Custom Error Classes
Located inbackend/src/utils/errors/index.js:
| Class | HTTP Code | Purpose |
|---|---|---|
ValidationError | 400 | Invalid input data or Zod validation failure. |
UnauthorizedError | 401 | Missing or invalid authentication token. |
ForbiddenError | 403 | Authenticated user lacks required permissions. |
NotFoundError | 404 | Resource does not exist. |
ConflictError | 409 | Resource already exists or state conflict. |
RateLimitError | 429 | Too many requests from a single client. |
Global Error Handler
TheerrorHandler middleware (backend/src/middlewares/errorHandler.js) is the final stop for every request.
- Sentry Integration: If
SENTRY_DSNis configured, all 500+ errors are automatically reported to Sentry. - Logging: All errors are logged locally via the
loggerService. - Formatting: Errors are transformed into a standard JSON structure using
errorFormatter. - Production Safety: In production, stack traces are stripped from 500 errors to avoid leaking internal details.
Response Formatters
We use two utility objects inbackend/src/utils/formatters/ to ensure the API always returns a predictable structure.
Success Response (successFormatter.js)
Used for all non-error responses.
formatSuccess(data, message, code): For single objects or simple operations.formatList(items, total, page, limit): specifically for paginated lists, adding apaginationmetadata object.
Error Response (errorFormatter.js)
Best Practices
- Never
res.send()directly: Always use the formatters in controllers. - Throw, don’t return: In services and repositories, throw the appropriate
BaseError. ThecatchAsyncwrapper in controllers will automatically pass it to the global error handler. - Operational vs. Programmer Errors:
BaseErrorsubclasses are considered “operational” (expected). Everything else is a “programmer error” and will result in a 500 status code.

