← Назад к вопросам

Зачем использовал кастомные исключения

2.3 Middle🔥 111 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Кастомные исключения в Java: зачем они нужны

Кастомные исключения — это один из мощнейших инструментов для написания чистого, понятного и maintainable кода. Это не просто «красивость», а архитектурное решение, которое серьёзно влияет на качество приложения.

Семантика и выразительность

Основная причина: кастомные исключения позволяют выразить конкретную ошибку вашего домена. Вместо того, чтобы писать:

// Плохо: непонятно, что пошло не так
try {
    user = userRepository.findById(id);
} catch (Exception e) {
    // Что случилось? NoSQL исключение? Timeout? Parsing ошибка?
}

Можно написать:

// Хорошо: ясная семантика
public class UserNotFoundException extends RuntimeException {
    private final UUID userId;
    
    public UserNotFoundException(UUID userId) {
        super("User not found with id: " + userId);
        this.userId = userId;
    }
    
    public UUID getUserId() {
        return userId;
    }
}

// Использование
try {
    user = userRepository.findById(id);
} catch (UserNotFoundException e) {
    // Точно знаю, что это конкретный случай
    log.warn("Attempted access to non-existent user: {}", e.getUserId());
    return ResponseEntity.notFound().build();
}

Код сразу становится понятнее, потому что ошибка имеет доменное название.

Обработка разных типов ошибок

Это критично в сложных системах. Разные ошибки требуют разных действий:

public class PaymentService {
    
    public void processPayment(Order order) {
        try {
            validateOrder(order);           // может выбросить ValidationException
            charge(order);                   // может выбросить PaymentGatewayException
            updateInventory(order);          // может выбросить InventoryException
        } catch (ValidationException e) {
            // Это ошибка клиента — вернуть 400
            throw new BadRequestException(e.getMessage(), e);
        } catch (PaymentGatewayException e) {
            // Это ошибка системы — логировать, отправить алерт
            log.error("Payment gateway failed", e);
            notifyOps("Payment processing failed: " + e.getMessage());
            throw new PaymentProcessingException("Payment failed, please try again", e);
        } catch (InventoryException e) {
            // Откатить платёж, вернуть средства
            chargeService.refund(order);
            throw new InventoryException("Out of stock, refund processed", e);
        }
    }
}

Высокоуровневый код может красиво обрабатывать разные сценарии, потому что исключения кодируют информацию об ошибке.

REST контроллер пример

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    
    @PostMapping
    public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
        try {
            Order order = orderService.createOrder(request);
            return ResponseEntity.ok(new OrderResponse(order));
        } catch (InvalidOrderException e) {
            return ResponseEntity.badRequest()
                .body(new ErrorResponse("Invalid order: " + e.getMessage()));
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        } catch (OutOfStockException e) {
            return ResponseEntity.status(409)
                .body(new ErrorResponse("Out of stock: " + e.getProductId()));
        } catch (PaymentProcessingException e) {
            return ResponseEntity.status(500)
                .body(new ErrorResponse("Payment failed, please try again"));
        }
    }
}

Каждое исключение соответствует HTTP статус коду и определённому ответу.

Иерархия исключений как архитектура

Кастомные исключения должны иметь иерархию:

// Базовое доменное исключение
public abstract class DomainException extends RuntimeException {
    public DomainException(String message) {
        super(message);
    }
    
    public DomainException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Ошибки валидации
public class ValidationException extends DomainException {
    public ValidationException(String message) {
        super(message);
    }
}

public class InvalidOrderException extends ValidationException {
    private final List<String> errors;
    
    public InvalidOrderException(List<String> errors) {
        super("Order validation failed: " + String.join(", ", errors));
        this.errors = errors;
    }
    
    public List<String> getErrors() {
        return errors;
    }
}

// Ошибки ресурсов
public class ResourceNotFoundException extends DomainException {
    public ResourceNotFoundException(String resourceType, String id) {
        super(resourceType + " not found: " + id);
    }
}

// Ошибки бизнес-логики
public class BusinessLogicException extends DomainException {
    public BusinessLogicException(String message) {
        super(message);
    }
}

public class InsufficientFundsException extends BusinessLogicException {
    private final BigDecimal available;
    private final BigDecimal required;
    
    public InsufficientFundsException(BigDecimal available, BigDecimal required) {
        super("Insufficient funds: available " + available + ", required " + required);
        this.available = available;
        this.required = required;
    }
}

Теперь выше по стеку можно ловить по иерархии:

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<?> handleValidation(ValidationException e) {
        return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleNotFound(ResourceNotFoundException e) {
        return ResponseEntity.notFound().build();
    }
    
    @ExceptionHandler(BusinessLogicException.class)
    public ResponseEntity<?> handleBusinessLogic(BusinessLogicException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(new ErrorResponse(e.getMessage()));
    }
}

Отладка и логирование

Кастомные исключения облегчают отладку:

public class PaymentProcessingException extends DomainException {
    private final String transactionId;
    private final String paymentGateway;
    private final long timestamp;
    
    public PaymentProcessingException(
        String message, 
        String transactionId, 
        String paymentGateway,
        Throwable cause) {
        super(message, cause);
        this.transactionId = transactionId;
        this.paymentGateway = paymentGateway;
        this.timestamp = System.currentTimeMillis();
    }
    
    public String toLogString() {
        return String.format(
            "Payment failed [txId=%s, gateway=%s, timestamp=%d]: %s",
            transactionId, paymentGateway, timestamp, getMessage()
        );
    }
}

// Логирование
log.error(exception.toLogString(), exception);

Выводы

Кастомные исключения:

  1. Улучшают читаемость — код ясно выражает, что может пойти не так
  2. Упрощают обработку — разные ошибки обрабатываются по-разному
  3. Документируют API — вызывающий код знает, какие исключения ожидать
  4. Облегчают отладку — содержат релевантный контекст ошибки
  5. Архитектурны — правильная иерархия отражает структуру системы

Не используй checked исключения для доменных ошибок (наследуй от RuntimeException), чтобы не усложнять API.