Skip to main content
Proper error handling is crucial for building robust and maintainable applications. This document outlines our approach to handling errors across use cases.

Core Principles

Our error handling approach is based on the following principles:
1

Domain-Centric

• Errors should be expressed in domain terms, not technical terms
2

Informative

• Errors should provide enough context to understand what went wrong
3

Actionable

• Errors should give hints on how to fix the issue when possible
4

Centralized

• Error handling should be consistent across the application
5

Layered

• Each layer should handle errors appropriate to its level of abstraction
6

Traceable

• The errors should be easily traceable to understand exactly where it was triggered from

Error Types

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)
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 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
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;
  }
}

Error Handling by Layer

The domain layer should:• Define domain-specific error types• Throw errors when business rules are violated• Not catch or handle errors (pass them up)
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...
  }
}
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
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;
    }
  }
}
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
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);
    }
  }
}
The presentation layer should:• Catch all errors• Translate domain errors to appropriate HTTP responses• Hide technical details from clients• Provide helpful error messages
@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"
      );
    }
  }
}

Exception Filters

NestJS has a powerful exception filter system that allows us to centralize error handling, We can create custom exception filters for common error types:
@Catch(LocationNotFoundError)
export class LocationNotFoundExceptionFilter implements ExceptionFilter {
  catch(exception: LocationNotFoundError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    response.status(404).json({
      statusCode: 404,
      message: exception.message,
      error: "Not Found",
    });
  }
}
These filters can be registered globally or per controller:
// Global registration
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new LocationNotFoundExceptionFilter());

// Controller-level registration
@Controller("orders")
@UseFilters(LocationNotFoundExceptionFilter)
export class OrderController {}

Logging Guidelines

Proper logging is essential for debugging and monitoring. We follow these guidelines:
1

Contextual Information

• Include relevant IDs, parameters, and stack traces
2

Severity Levels

• Use appropriate log levels (error, warn, info, debug)
3

Sensitive Data

• Never log sensitive information (passwords, API keys, etc.)
4

Structured Logging

• Use structured logging for easier analysis
// Good logging
this.logger.error("Failed to create order", {
  orderId: order.id,
  customerId: order.customerId,
  error: {
    name: error.name,
    message: error.message,
    stack: error.stack,
  },
});

// Bad logging
this.logger.error(`Failed to create order: ${error}`);

Testing Error Scenarios

When testing, it’s important to verify error scenarios:
describe("CreateOrderUseCase", () => {
  it("should throw LocationNotFoundError when location does not exist", async () => {
    // Arrange
    const order = OrderFactory.createValid();
    mockLocationRepository.findById.mockResolvedValue(null);

    // Act & Assert
    await expect(useCase.execute(order)).rejects.toThrow(LocationNotFoundError);
    expect(mockLocationRepository.findById).toHaveBeenCalledWith(
      order.locationId
    );
    expect(mockOrderRepository.create).not.toHaveBeenCalled();
  });
});

Best Practices

1

Be Specific

• Create specific error classes for different error cases
2

Add Context

• Include relevant information in error messages
3

Don't Catch Everything

• Only catch errors you can handle meaningfully
4

Centralize Translation

• Translate errors to HTTP responses in controllers or filters
5

Log Appropriately

• Log errors with proper context and severity
6

Test Error Cases

• Write tests for error scenarios
7

Use Custom Errors

• Extend built-in error classes for domain errors