Вопрос 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

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

  • [[14. Когда стоит создавать свои исключения]]
  • [[15. Что лучше наследоваться от Exception или RuntimeException]]
  • [[17. Как правильно логировать исключения]]
  • [[2. Что такое checked exception и когда его использовать]]
  • [[3. Что такое unchecked exception (Runtime Exception)]]