Skip to main content

Error handling

This document describes how failures are represented and returned in the API with Fastify, using the users module (apps/api/src/modules/users) as the concrete example.

Two kinds of failure

  1. Expected failures (unauthorized access, missing profile/user, invalid domain input) are returned explicitly by handlers with reply.code(...).send(...).
  2. Unexpected failures (thrown/rejected errors from use cases, repositories, infrastructure, or bugs) bubble to the global Fastify setErrorHandler in apps/api/src/index.ts.

Global Fastify error handler (index.ts)

The app defines a central app.setErrorHandler(...) that applies to uncaught route errors.

Behavior:

  • Reads error.statusCode only if it is numeric and >= 400; otherwise defaults to 500.
  • Returns { message: "Internal server error" } for 5xx.
  • Returns { message: error.message } for 4xx.
  • Logs http.request.error with request and tracing metadata (requestId, method, path, statusCode, err, trace_id, span_id).

This means thrown internal errors are masked from clients but remain visible in logs.

HTTP adapters (modules/*/adapters/http/handlers.ts)

Handlers use explicit control flow for known outcomes.

Common patterns in users handlers:

  • 401 when auth context is missing or incomplete ({ message: "Unauthorized" }).
  • 404 when a use case returns null ({ message: ... }).
  • 422 for domain-level input rules not covered by schema alone (for example, update profile with no patchable fields).
  • 200/201 for successful outcomes.

Handlers generally do not wrap use case calls in broad try/catch, so unknown failures continue to the global error handler.

Application and persistence layers

  • Use cases are thin and often return null for “not found”, leaving HTTP status mapping to adapters.
  • Repository code may use narrow catches for known infra races (for example Prisma P2002 uniqueness conflicts) and rethrow unknown errors.
  • Any rethrown/uncaught error eventually becomes a Fastify error-handler response.

Interaction with logging

Request logging is implemented in apps/api/src/index.ts:

  • http.request.error is emitted by the global error handler.
  • http.request.completed is emitted in onResponse for completed requests (except OPTIONS), with status and duration.
  • Completion logs are leveled by response status (info for 2xx/3xx, warn for 4xx, error for 5xx).

A failed request can emit both lines: one for error details and one for final response timing/status.

Summary

LayerExpected failureUnexpected failure
Handlersreply.code(...).send(...)Throw/reject -> Fastify global setErrorHandler
Use casesReturn null or structured resultPropagate
RepositoryNarrow catch for known casesRe-throw/propagate
Global app (index.ts)N/ANormalize status/body + log error

When adding routes, keep this pattern: encode expected outcomes directly in handlers, keep catches narrow and intentional, and let Fastify's global error handler own fallback behavior.