Proper error handling is crucial for building robust and maintainable
applications. This document outlines our approach to handling errors across
use cases.
Domain errors represent business rule violations or invalid operations within our domain. They should be:• Named descriptively (e.g., LocationNotFoundError, InsufficientInventoryError)• Specific to a single failure mode• Rich in context (e.g., include relevant IDs, expected/actual values)
Copy
export class LocationNotFoundError extends Error { constructor(locationId: string) { super(`Location with ID ${locationId} not found`); this.name = "LocationNotFoundError"; }}export class OrderAlreadyProcessedError extends Error { constructor(orderId: string) { super(`Order ${orderId} has already been processed and cannot be modified`); this.name = "OrderAlreadyProcessedError"; }}
Infrastructure Errors
Infrastructure errors represent failures in external systems or services. They should be:• Wrapped in domain-specific errors when propagated to upper layers• Logged with technical details• Abstracted away from domain logic
Copy
export class DatabaseConnectionError extends Error { constructor(operation: string, cause?: Error) { super(`Failed to connect to database during ${operation}`); this.name = "DatabaseConnectionError"; this.cause = cause; }}export class ExternalServiceError extends Error { constructor(service: string, operation: string, cause?: Error) { super(`Failed to perform ${operation} on ${service}`); this.name = "ExternalServiceError"; this.cause = cause; }}
The domain layer should:• Define domain-specific error types• Throw errors when business rules are violated• Not catch or handle errors (pass them up)
Copy
export class Order { validate(): void { if (this.items.length === 0) { throw new EmptyOrderError(this.id); } if (this.totalAmount <= 0) { throw new InvalidOrderAmountError(this.id, this.totalAmount); } // More validation rules... }}
Application Layer (Use Cases)
The application layer should:• Validate inputs• Catch infrastructure errors and translate them to domain errors when appropriate• Let domain errors pass through• Log errors with proper context
Copy
export class CreateOrderUseCase { async execute(order: Order): Promise<Order> { try { // Validate order order.validate(); // Check if location exists const location = await this.locationRepository.findById(order.locationId); if (!location) { throw new LocationNotFoundError(order.locationId); } // Create order return await this.orderRepository.create(order); } catch (error) { // Translate infrastructure errors to domain errors if (error instanceof DatabaseError) { throw new OrderCreationFailedError( order.id, `Failed to create order due to database error: ${error.message}` ); } // Log and rethrow this.logger.error("Failed to create order", { order, error }); throw error; } }}
Infrastructure Layer
The infrastructure layer should:• Catch low-level errors (e.g., database errors, network errors)• Translate them to infrastructure-specific errors• Add context about the operation that failed• Not leak implementation details
Copy
export class PrismaOrderRepository implements IOrderRepository { async create(order: Order): Promise<Order> { try { const orderData = await this.prisma.order.create({ data: { // Map from domain to database model }, }); return this.mapToDomain(orderData); } catch (error) { if (error instanceof PrismaClientKnownRequestError) { if (error.code === "P2002") { throw new DuplicateOrderError(order.id); } } throw new DatabaseError("Failed to create order", error); } }}
Presentation Layer
The presentation layer should:• Catch all errors• Translate domain errors to appropriate HTTP responses• Hide technical details from clients• Provide helpful error messages
Copy
@Controller("orders")export class OrderController { @Post() async createOrder( @Body() createOrderDto: CreateOrderDto ): Promise<OrderResponseDto> { try { const order = OrderMapper.toDomain(createOrderDto); const createdOrder = await this.createOrderUseCase.execute(order); return OrderMapper.toDto(createdOrder); } catch (error) { // Map domain errors to HTTP responses if (error instanceof LocationNotFoundError) { throw new NotFoundException(error.message); } if ( error instanceof EmptyOrderError || error instanceof InvalidOrderAmountError ) { throw new BadRequestException(error.message); } if (error instanceof OrderCreationFailedError) { throw new UnprocessableEntityException(error.message); } // Unexpected errors this.logger.error("Unexpected error creating order", { error, dto: createOrderDto, }); throw new InternalServerErrorException( "An unexpected error occurred while processing your request" ); } }}
NestJS has a powerful exception filter system that allows us to centralize error handling, We can create custom exception filters for common error types: