Вопрос 14 · Раздел 7

Когда стоит создавать свои исключения?

Создавайте своё исключение, когда:

Версии по языкам: English Russian Ukrainian

Junior Level

Когда СТОИТ создавать

1. Для бизнес-ошибок:

public class InsufficientFundsException extends RuntimeException {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

2. Когда нужно передать дополнительную информацию:

public class OrderTimeoutException extends RuntimeException {
    private final Long orderId;
    private final int timeoutSeconds;
    
    public OrderTimeoutException(Long orderId, int timeoutSeconds) {
        super("Order " + orderId + " timed out after " + timeoutSeconds + "s");
        this.orderId = orderId;
        this.timeoutSeconds = timeoutSeconds;
    }
}

3. Когда стандартные исключения не подходят:

// Вместо общего IllegalArgumentException
throw new InvalidEmailException("Email must contain @ and domain");

Когда НЕ СТОИТ создавать

Не создавайте, если подходят стандартные:

// Не нужно своё исключение
if (age < 0) throw new IllegalArgumentException("Age < 0");

// Не нужно своё исключение
if (user == null) throw new NullPointerException("User is null");

Простое правило

Создавайте своё исключение, когда:

  • Это бизнес-ошибка (не баг кода)
  • Нужны дополнительные поля
  • Нужен отдельный HTTP-статус

Кастомное или стандартное исключение?

Создавайте кастомное, если нужно:

  • Отдельный catch-блок для этого типа ошибки
  • Отдельный HTTP-статус (404, 409, 422)
  • Дополнительные поля (orderId, retryAfter)

Используйте стандартное, если:

  • Достаточно логировать сообщение
  • Ошибка однотипная и не требует специальной обработки

Middle Level

Архитектурная цена кастомных исключений

Каждое новое исключение — это класс в Metaspace. В больших системах с тысячами исключений это увеличивает время старта и потребление памяти.

Когда создавать — детальнее

1. Бизнес-валидация:

throw new CouponExpiredException("Coupon expired on " + expiryDate);
// @ControllerAdvice превратит в 400 Bad Request с JSON

2. Exception Translation на границе слоёв:

try {
    repository.save(order);
} catch (SQLException e) {
    throw new DataAccessException("Failed to save order", e);
}

3. Информативность: Нужны доп. поля: retryAfter, errorCode, entityId.

Когда НЕ создавать

1. Flow Control — антипаттерн:

// ❌ АНТИПАТТЕРН: исключения как if-else
try {
    User user = findUser(id); // бросает UserFoundException если найден
    // следующий шаг...
} catch (UserFoundException e) {
    // «Нормальный» ход выполнения через исключение!
}

// ✅ Вместо этого:
Optional<User> user = findUser(id);
if (user.isPresent()) { /* следующий шаг */ }

Бенчмарки показывают 100-1000x замедление. На абсолютных значениях: возврат Optional ~10нс, создание исключения ~1-5 мкс (зависит от глубины стека и JVM).

2. Дублирование JDK:

// Не нужно, если IllegalArgumentException описывает ситуацию
throw new InvalidAgeException("Age < 0");
// Лучше:
throw new IllegalArgumentException("Age must be positive, got: " + age);

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

RuntimeException
  └── BusinessException
        ├── OrderException
        │     ├── OrderNotFoundException
        │     └── OrderAlreadyShippedException
        └── PaymentException
              ├── InsufficientFundsException
              └── PaymentProviderException

Senior Level

Domain-Driven Exceptions

В качественной архитектуре исключение — это событие доменной модели, а не просто “ошибка”.

Immutable Exceptions для Highload

Для часто возникающих бизнес-ошибок, которые не требуют отладки по стеку:

public class FastValidationException extends RuntimeException {
    public FastValidationException(String message) {
        super(message, null, false, false);
    }
    
    // Предопределённые экземпляры
    public static final FastValidationException EMPTY_EMAIL = 
        new FastValidationException("Email is required");
    public static final FastValidationException INVALID_PHONE = 
        new FastValidationException("Invalid phone format");
}

Exception Hierarchy и группировка

Стройте иерархию для перехвата группами:

// Можно ловить все бизнес-ошибки
catch (BusinessException e) { return 400; }

// Или конкретнее
catch (OrderNotFoundException e) { return 404; }

Serialization и микросервисы

public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    // Без этого — InvalidClassException при десериализации
}

PII Leak и безопасность

Не добавляйте поля типа creditCardNumber в исключения. Если попадёт в логи — нарушение PCI DSS.

PII (Personally Identifiable Information) – персональные данные. PCI DSS – стандарт безопасности платёжных карт. Попадание данных карт в логи = нарушение стандарта.

Metric-driven analysis

// Считайте через Micrometer
meterRegistry.counter("exceptions.coupon_expired").increment();

// Резкий рост CouponExpiredException — бизнес-индикатор

Error Codes

Каждое кастомное исключение — уникальный строковый код:

public enum ErrorCode {
    ERR_USER_NOT_FOUND("USR-001"),
    ERR_INSUFFICIENT_FUNDS("PAY-002"),
    ERR_COUPON_EXPIRED("COU-003");
    
    private final String code;
}

Фронтенд использует код для локализации.


🎯 Шпаргалка для интервью

Обязательно знать:

  • Создавайте кастомное исключение для бизнес-ошибок, когда нужны дополнительные поля или отдельный HTTP-статус
  • НЕ используйте исключения как flow control (антипаттерн) — это в 100-1000x медленнее if/else или Optional
  • Стройте иерархию: BusinessExceptionOrderExceptionOrderNotFoundException для группировки catch-блоков
  • Каждое кастомное исключение — уникальный строковый код (ERR-001) для локализации и фронтенда
  • Не раскрывайте PII в сообщениях (пароли, номера карт) — нарушение PCI DSS
  • Для highload: immutable exceptions с предопределёнными экземплярами и writableStackTrace = false

Частые уточняющие вопросы:

  • Как решить: кастомное или стандартное исключение? — Если нужен отдельный catch-блок, HTTP-статус или доп. поля — кастомное; если достаточно сообщения — стандартное
  • Почему исключения как flow control — антипаттерн? — Создание stack trace стоит 1-5 мкс, а Optional — ~10 нс; плюс код становится нечитаемым
  • Как организовать иерархию исключений? — Общий родитель (BusinessException) для группового перехвата + конкретные подклассы для отдельных HTTP-статусов
  • Что делать с исключениями в микросервисах? — На границе переводить в стандартный формат (RFC 7807), внутри сервиса использовать кастомные

Красные флаги (НЕ говорить):

  • “Использую исключения вместо if-else” — Это антипаттерн с серьёзным падением производительности
  • “Создаю исключение для каждой валидации” — IllegalArgumentException обычно достаточно
  • “Не забочусь о безопасности сообщений” — PII в логах = нарушение GDPR/PCI DSS

Связанные темы:

  • [[13. Можно ли создавать кастомные исключения]]
  • [[15. Что лучше наследоваться от Exception или RuntimeException]]
  • [[15. Что такое stack trace]]
  • [[17. Как правильно логировать исключения]]
  • [[19. Почему не стоит глотать исключения (catch empty)]]