Error Handling Strategy
Relevant source files
The following files were used as context for generating this wiki page:
- apps/gateway/src/filters/http-exception.filter.ts
- libs/shared/filters/rpc-exception.filter.ts
- apps/gateway/src/filters/http-exception.filter.spec.ts
- libs/shared/filters/rpc-exception.filter.spec.ts
- libs/shared/filters/error-parser.spec.ts
- libs/shared/filters/error-detector.spec.ts
- libs/shared/filters/error-response-builder.spec.ts
- apps/product-app/src/categories/categories.service.spec.ts
- apps/gateway/test/gateway.e2e-spec.ts
The project employs a robust, multi-layered error handling strategy designed for a microservices architecture. The core principle is to centralize error processing at the API Gateway, ensuring that clients receive consistent, well-formatted HTTP error responses, regardless of which downstream microservice originated the error. This is achieved by propagating errors from microservices as RpcExceptions over the NATS message broker, which are then caught and processed by a global filter in the API Gateway.
This approach decouples error formatting from business logic within the microservices. Services focus on identifying and throwing domain-specific errors, while the Gateway is solely responsible for translating these into a client-friendly format. The strategy utilizes a shared library of error handling utilities for parsing, refining, and building error responses, promoting code reuse and consistency across the system.
Architecture Overview
The error handling flow is initiated when a microservice encounters an issue. Instead of returning a standard response, it throws an RpcException. This exception is serialized and sent over NATS to the API Gateway. The Gateway, equipped with a global exception filter, intercepts this error, parses its content, and transforms it into a standard HTTP error response before sending it back to the original client.
The following diagram illustrates the end-to-end error handling flow:
This diagram shows how an error travels from a microservice, through the NATS broker, to the API Gateway's exception filter, where it is parsed and formatted into a final HTTP response.
Sources: libs/shared/filters/rpc-exception.filter.ts, apps/product-app/src/categories/categories.service.spec.ts
Key Components
The error handling system is composed of several key classes, primarily residing in the API Gateway and the libs/shared library.
| Component | Location | Responsibility |
|---|---|---|
RpcException | @nestjs/microservices | The standard class for communicating errors between microservices. |
AllRpcExceptionsFilter | libs/shared/filters | A global filter in the Gateway that catches all RpcExceptions and other errors from microservices. |
HttpExceptionFilter | apps/gateway/src/filters | A standard NestJS filter that catches HttpExceptions originating within the Gateway itself. |
ErrorParser | libs/shared/filters | A utility class to parse various error formats (string, object) into a standardized ParsedError object. |
ErrorDetector | libs/shared/filters | A helper class used by ErrorParser to infer specific HTTP status codes from generic error messages by matching keywords. |
ErrorResponseBuilder | libs/shared/filters | A static class that constructs the final JSON error response for both HTTP and RPC contexts. |
Sources: libs/shared/filters/rpc-exception.filter.ts, apps/gateway/src/filters/http-exception.filter.ts, libs/shared/filters/error-parser.spec.ts, libs/shared/filters/error-detector.spec.ts, libs/shared/filters/error-response-builder.spec.ts
Detailed Flow and Logic
1. Error Origination in Microservices
Microservices are responsible for their own business logic and validation. When an error occurs (e.g., resource not found, validation failure), the service throws an RpcException. This is a standard practice across all services to ensure errors are transportable over NATS.
For example, if a category is not found by its ID, the CategoriesService throws a 404 error.
// Example from a service test
it('should throw RpcException when category not found', async () => {
mockPrismaService.category.findUnique.mockResolvedValue(null);
await expect(service.getById({ id: 'non-existent' })).rejects.toThrow(RpcException);
try {
await service.getById({ id: 'non-existent' });
} catch (error) {
expect(error).toBeInstanceOf(RpcException);
expect((error as RpcException).getError()).toMatchObject({
statusCode: 404,
message: 'Category with ID non-existent not found',
});
}
});
Sources: apps/product-app/src/categories/categories.service.spec.ts:90-103
2. Exception Filtering in the API Gateway
The API Gateway uses two primary filters to handle all exceptions.
HttpExceptionFilter
This filter is responsible for catching exceptions of type HttpException that are thrown directly within the API Gateway's context. This typically includes errors from authentication guards, validation pipes, or rate limiters. It formats the exception into a JSON object and logs the error details.
Sources: apps/gateway/src/filters/http-exception.filter.ts, apps/gateway/src/filters/http-exception.filter.spec.ts
AllRpcExceptionsFilter
This is the most critical component of the distributed error handling strategy. It's a global filter that catches exceptions propagated from the microservices layer. It can handle both structured RpcException objects and generic Error types.
The filter's logic distinguishes between the execution context:
- HTTP Context: If the request is from an HTTP client, it uses a chain of helpers (
ErrorParser,ErrorDetector,ErrorResponseBuilder) to create a detailed HTTP error response. - RPC Context: If the error occurs in a pure RPC-to-RPC call, it builds a simpler RPC error response.
The following sequence diagram details the interactions within the AllRpcExceptionsFilter when handling an error from a microservice in an HTTP context.
3. Shared Error Handling Utilities
The AllRpcExceptionsFilter relies on a set of reusable helper classes from the @shared library to process errors.
ErrorParser
This utility normalizes different error formats into a consistent ParsedError object ({ statusCode, message, details }). It can parse:
- Objects: Extracts
statusCodeandmessagefrom object-based errors. - Strings: Uses
ErrorDetectorto infer a status code and message from a string.
Sources: libs/shared/filters/error-parser.spec.ts
ErrorDetector
This class "enhances" generic 500 Internal Server Errors by attempting to infer a more accurate status code from the error message. It searches for keywords within the message string. This is particularly useful for translating generic NATS client errors (like timeouts) into meaningful HTTP statuses.
The default patterns are:
| Keywords (case-insensitive) | Inferred Status Code | Inferred Message |
|---|---|---|
empty response | 503 Service Unavailable | Service temporarily unavailable |
timeout | 408 Request Timeout | Request timeout - service did not respond in time |
not found | 404 Not Found | (Original message is preserved) |
unauthorized | 401 Unauthorized | (Original message is preserved) |
forbidden | 403 Forbidden | (Original message is preserved) |
bad request | 400 Bad Request | (Original message is preserved) |
conflict | 409 Conflict | (Original message is preserved) |
Sources: libs/shared/filters/error-detector.spec.ts:20-111
ErrorResponseBuilder
This static class is the final step in the process. It takes the statusCode, message, and details from the parsed error and constructs the final JSON payload.
buildHttpResponse: Creates a response for clients. It includes atimestampand anerrorfield (e.g., "Internal Server Error") for 5xx status codes.buildRpcResponse: Creates a simpler response for inter-service communication.
Sources: libs/shared/filters/error-response-builder.spec.ts
Final Error Response Structure
When an error is caught and processed by the gateway, the client receives a JSON response with a consistent structure.
| Field | Type | Description |
|---|---|---|
statusCode | number | The HTTP status code (e.g., 404, 500). |
message | string | A human-readable description of the error. |
timestamp | string | The ISO 8601 timestamp when the error occurred. |
details | any | Optional field containing additional error context or validation details. Present only if provided. |
error | string | An optional, short description of the HTTP status (e.g., "Not Found", "Internal Server Error"). Included for 5xx errors. |
Example 404 Response:
{
"statusCode": 404,
"message": "User not found",
"timestamp": "2024-05-21T10:00:00.000Z"
}
Example 503 Response from a timeout:
{
"statusCode": 503,
"message": "Service temporarily unavailable",
"error": "Internal Server Error",
"timestamp": "2024-05-21T10:05:00.000Z"
}
Sources: libs/shared/filters/error-response-builder.spec.ts:8-20, 34-42, apps/gateway/test/gateway.e2e-spec.ts:193-211
Conclusion
The error handling strategy effectively centralizes and standardizes error responses in a distributed system. By leveraging NestJS exception filters and a shared library of helper utilities, the architecture ensures that microservices can focus on their core logic while the API Gateway provides a consistent and predictable error-reporting interface to clients. This separation of concerns simplifies development in individual microservices and enhances the overall maintainability and robustness of the application.