Чи можна створювати власні винятки?
Кастомний виняток — це звичайний клас, що успадковує 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
Пов’язані теми:
- [[Коли варто створювати свої винятки]]
- [[Що краще: наслідувати Exception чи RuntimeException]]
- [[Як правильно логувати винятки]]
- [[Що таке checked виняток і коли його використовувати]]
- [[Що таке unchecked виняток (Runtime Exception)]]