Питання 13 · Розділ 7

Чи можна створювати власні винятки?

Кастомний виняток — це звичайний клас, що успадковує Exception або RuntimeException.

Мовні версії: English Russian Ukrainian

Junior Level

Так, можна!

Кастомний виняток — це звичайний клас, що успадковує Exception або RuntimeException.

Кастомний = створений вами для вашої предметної області. Стандартний = з JDK (IOException, IllegalArgumentException). Кастомні несуть доменний сенс: OrderNotFoundException каже більше, ніж IllegalArgumentException.

// Checked виняток
public class UserNotFoundException extends Exception {
    public UserNotFoundException(String message) {
        super(message);
    }
}

// Unchecked виняток
public class InsufficientFundsException extends RuntimeException {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

Як використовувати

public User findUser(Long id) {
    User user = repository.findById(id);
    if (user == null) {
        throw new UserNotFoundException("User not found: " + id);
    }
    return user;
}

// Обробка
try {
    findUser(1L);
} catch (UserNotFoundException e) {
    System.out.println(e.getMessage());
}

Коли створювати checked, коли unchecked

  • Checked (extends Exception) — якщо помилка очікувана і відновлювана
  • Unchecked (extends RuntimeException) — для бізнес-помилок та помилок програмування

Поради

  • Успадковуйте від RuntimeException у більшості випадків
  • Давайте зрозумілі імена: UserNotFoundException, OrderAlreadyShippedException
  • Викликайте super(message) для передачі повідомлення

Коли НЕ створювати кастомні винятки

  1. Достатньо стандартногоIllegalArgumentException("Email invalid") читабельно
  2. Виняток використовується один раз – немає сенсу створювати клас заради одного throw
  3. «Паралельна ієрархія» – не створюйте Exception для кожного нового use case

Middle Level

Exception як DTO

У розподілених системах кастомний виняток слугує DTO для передачі помилки:

public class BusinessException extends RuntimeException {
    private final ErrorCode code;
    private final Map<String, Object> context = new HashMap<>();

    public BusinessException(ErrorCode code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException with(String key, Object value) {
        this.context.put(key, value);
        return this;
    }

    public ErrorCode getCode() { return code; }
    public Map<String, Object> getContext() { return context; }
}

Використання:

throw new BusinessException(INSUFFICIENT_FUNDS, "Low balance")
    .with("userId", 123)
    .with("balance", 0.50);

// Перевага .with(): можна додати контекст у місці кидання, // не створюючи конструктор з 10 параметрами. // Лямбда-стиль: throw new BusinessException(code, msg).with(“key”, value)

Domain-Driven Exceptions

У якісній архітектурі винятки поділяються на рівні:

  1. Інфраструктурні: DatabaseException, NetworkException
  2. Доменні: InsufficientFundsException, ProductOutOfStockException

Доменні винятки повинні бути інформативними — не просто “помилка”, а “користувач X не зміг купити товар Y”.

Проблема мікросервісів

Якщо сервіс A кидає OrderNotFoundException, а сервіс B не має цього класу в classpath — NoClassDefFoundError.

Рішення: на межах мікросервісів винятки перекладаються у стандартні структури (RFC 7807 Problem Details). Кастомні винятки живуть лише всередині сервісу.

RFC 7807 – стандарт формату JSON для повідомлень про помилки в HTTP API. Визначає поля: type, title, status, detail, instance. Spring підтримує через ProblemDetail (з Java 17).


Senior Level

Immutable & Stackless Exceptions для Highload

У Highload-системах створення Throwable дороге через fillInStackTrace():

public class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message, null, false, false); // writableStackTrace = false
    }
}

Тисячі винятків за секунду без навантаження на CPU.

Serialization UID

Завжди оголошуйте serialVersionUID:

public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    // ...
}

Без нього при зміні класу (додаванні поля) і десеріалізації старої версії з Redis-кешу — InvalidClassException.

Exception Masking та PII

Повідомлення кастомного винятку не повинно розкривати чутливі дані:

// ПОГАНО — пароль у логах
throw new AuthException("Failed login for user: " + username + " with password: " + password);

// ДОБРЕ
throw new AuthException("Authentication failed for user: " + username);

Global Error Handler у Spring Boot

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
        return ResponseEntity.status(400)
            .body(ErrorResponse.of(e.getCode(), e.getMessage(), e.getContext()));
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException e) {
        return ResponseEntity.status(404)
            .body(ErrorResponse.of("USER_NOT_FOUND", e.getMessage()));
    }
}

Діагностика

  • Log Correlation — повідомлення винятку повинно містити дані для фільтрації в ELK
  • Error Codes — кожен виняток повинен мати унікальний код (наприклад, ERR-001) для локалізації
  • Micrometer — рахуйте кожен кастомний виняток через метрики

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Кастомний виняток = звичайний клас, що успадковує Exception або RuntimeException
  • Успадковуйте від RuntimeException у більшості випадків (менше boilerplate)
  • extends Exception — якщо помилка очікувана і викликач може відновитися
  • У мікросервісах кастомні винятки живуть лише всередині сервісу; на межі — переклад у стандартні формати (RFC 7807)
  • Для highload використовуйте writableStackTrace = false щоб уникнути дорогого fillInStackTrace()
  • Завжди оголошуйте serialVersionUID для Serializable-сумісності

Часті уточнюючі запитання:

  • Коли НЕ створювати кастомний виняток? — Якщо достатньо стандартного (IllegalArgumentException), або виняток використовується один раз
  • Як передати контекст у винятку? — Додайте поля (orderId, errorCode) або використовуйте fluent-метод .with(key, value)
  • Що таке Exception Masking? — Виняток не повинен розкривати чутливі дані (паролі, PII) у повідомленні
  • Як обробляти кастомні винятки у Spring? — Через @RestControllerAdvice + @ExceptionHandler → маппінг на HTTP-статуси

Червоні прапорці (НЕ говорити):

  • “Створюю виняток для кожного use case” — Це веде до «паралельної ієрархії» і роздування коду
  • “Передаю пароль/PII у повідомленні винятку” — Порушення безпеки (PCI DSS, GDPR)
  • “Кастомні винятки автоматично мапляться на HTTP” — Потрібен @ControllerAdvice, без цього буде 500

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

  • [[Коли варто створювати свої винятки]]
  • [[Що краще: наслідувати Exception чи RuntimeException]]
  • [[Як правильно логувати винятки]]
  • [[Що таке checked виняток і коли його використовувати]]
  • [[Що таке unchecked виняток (Runtime Exception)]]