Можно ли создавать кастомные исключения?
Кастомное исключение — это обычный класс, наследующий Exception или RuntimeException.
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)для передачи сообщения
Когда НЕ создавать кастомные исключения
- Достаточно стандартного –
IllegalArgumentException("Email invalid")читаемо - Исключение используется один раз – нет смысла создавать класс ради одного throw
- «Параллельная иерархия» – не создавайте 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
В качественной архитектуре исключения делятся на уровни:
- Инфраструктурные:
DatabaseException,NetworkException - Доменные:
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)]]