Turn them into HTTP responses in one place, at the boundary of the app. The domain and application layers should know nothing about HTTP. They throw (or return) domain errors that describe the problem in business terms.
In a Nest or Express app, this place is one of these:
- a single exception filter
- an error middleware
- the controller itself
It maps each error type to a response:
OrderNotFoundErrorbecomes 404.OrderAlreadyPaidErrorbecomes 409 Conflict.InsufficientCreditErrorbecomes 422 or 409, depending on your API conventions.- Validation errors become 400.
- Any unknown error becomes a 500 with a generic message. The server logs the full error and stack trace.
A practical approach is a base DomainError class with a stable, machine-readable code. Then the mapper can use a simple lookup table. Clients can rely on the code instead of the message text. Other transports, such as a gRPC service or a queue consumer, can map the same domain errors to their own protocol.
- The domain throws business errors and knows nothing about HTTP.
- Map errors once at the boundary, in a filter, middleware or controller.
- A typical mapping is not found to 404, conflict to 409, and validation to 400 or 422.
- Unknown errors become a generic 500, and the server logs the details.
- Use stable error codes so other transports can map the same errors.
Read more