Когда стоит создавать свои исключения?
Создавайте своё исключение, когда:
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 - Стройте иерархию:
BusinessException→OrderException→OrderNotFoundExceptionдля группировки 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)]]