Питання 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

Пов’язані теми:

  • [[Чи можна створювати власні винятки]]
  • [[Що краще: наслідувати Exception чи RuntimeException]]
  • [[Що таке stack trace]]
  • [[Як правильно логувати винятки]]
  • [[Чому не варто ковтати винятки (catch empty)]]