Какие исключения нужно обрабатывать обязательно
Ответ на этот вопрос зависит от того, кто определяет обязательность — компилятор Java (javac) или архитектура надежности приложения.
🟢 Junior Level
Ответ на этот вопрос зависит от того, кто определяет обязательность — компилятор Java (javac) или архитектура надежности приложения.
1. С точки зрения компилятора Java:
Обязательными к обработке являются исключительно Checked Exceptions (проверяемые исключения):
- Это все классы, наследующиеся от
java.lang.Exception, кроме веткиjava.lang.RuntimeException. - Если метод вызывает код, выбрасывающий checked-исключение, разработчик обязан выбрать одно из двух действий:
- Обернуть вызов в конструкцию
try-catch. - Объявить проверяемое исключение в сигнатуре метода через ключевое слово
throws.
- Обернуть вызов в конструкцию
Если ни одно из действий не выполнено, исходный код не скомпилируется:
// ОШИБКА КОМПИЛЯЦИИ: Unhandled exception: java.io.IOException
public void readFile() {
Files.readAllBytes(Path.of("config.json"));
}
2. Сводная таблица обязательности обработки
| Категория исключений | Примеры | Обязательно компилятором? | Обязательно в архитектуре? |
|---|---|---|---|
| Checked Exceptions | IOException, SQLException, InterruptedException |
✅ Да (строго) | ✅ Да (локально или трансляция) |
| Unchecked Exceptions | NullPointerException, IllegalArgumentException |
❌ Нет | 🟡 Да (на границах слоев и потоков) |
| Errors | OutOfMemoryError, StackOverflowError |
❌ Нет | 🛑 Запрещено перехватывать |
🟡 Middle Level
Обязательная обработка на границах системы (System Boundaries)
Хотя компилятор не заставляет писать try-catch для RuntimeException, в реальном промышленном приложении необработанное исключение:
- Завершит поток выполнения (Thread death).
- Вызовет утечку деталей реализации клиенту (Internal Server Error 500 со стек-трейсом).
- Приведет к зависанию распределенной транзакции или сообщения в очереди брокера (Kafka / RabbitMQ).
Поэтому на периметре системы (границы слоев) обработка unchecked-исключений является архитектурно обязательной:
- В REST API: Верхнеуровневый
@RestControllerAdviceобязан перехватывать все непредвиденныеExceptionи отдавать клиенту обезличенный RFC 9457ProblemDetail. - В Consumer Kafka / RabbitMQ: Обработчик очереди обязан перехватывать ошибки парсинга и бизнес-сбои, направляя битые сообщения в Dead Letter Queue (DLQ).
Почему перехват Error и Throwable категорически не рекомендуется?
Начинающие разработчики часто пишут:
// ❌ ОПАСНЫЙ АНТИПАТТЕРН:
try {
doSomeHeavyComputation();
} catch (Throwable t) { // Перехватывает Error, OutOfMemoryError, ThreadDeath!
log.error("Error occurred", t);
}
Чем это грозит:
java.lang.Error(в частностиOutOfMemoryError,InternalError,UnknownError) сигнализирует о катастрофическом отказе среды выполнения JVM.- При возникновении
OutOfMemoryErrorпамять кучи (Heap) исчерпана, внутренние структуры JVM могут быть повреждены, а мониторы синхронизации разблокированы в некорректном состоянии. - Попытка «проглотить»
OutOfMemoryErrorи продолжить работу приложения переводит сервис в режим «зомби»: запросы виснут, блокировки не освобождаются, инстанс не отвечает на Health Check. Единственное правильное действие приError— дать процессу упасть и позволить Kubernetes / Systemd перезапустить под.
🔴 Senior Level
Необработанные исключения на границе многопоточности
Если внутри фонового потока (Worker Thread) выбрасывается unchecked-исключение, поведение зависит от механизма запуска:
new Thread(runnable).start(): Поток немедленно завершает работу. По умолчанию стек-трейс печатается вSystem.err, а само приложение может даже не зафиксировать сбой в лог-файле.ExecutorService.execute(Runnable): Поток завершается аварийно, пул потоков создает новый поток взамен погибшего.ExecutorService.submit(Callable): Исключение перехватывается реализациейFutureTaskи сохраняется внутри. Если вызывающий код не вызвал методfuture.get(), исключение бесследно растворяется, и никто не узнает об аварии.
Production решение: Установка UncaughtExceptionHandler
Для предотвращения невидимой гибели потоков на уровне инициализации приложения регистрируется обработчик:
package com.example.init;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class GlobalExceptionHandlerInitializer implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandlerInitializer.class);
@Override
public void run(String... args) {
// Перехватывает все необработанные исключения в любых потоках JVM
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
if (throwable instanceof Error) {
log.error("FATAL: JVM Error in thread [{}]. Initiating emergency shutdown.",
thread.getName(), throwable);
// Завершаем процесс при фатальных системных ошибках
System.exit(1);
} else {
log.error("CRITICAL: Uncaught exception in thread [{}]",
thread.getName(), throwable);
}
});
}
}
Особый статус InterruptedException: контракт кооперативной многопоточности
InterruptedException — это проверяемое исключение, требующее обязательной архитектурной дисциплины.
Поток в Java нельзя безопасно «убить» извне (метод Thread.stop() объявлен deprecated и удален). Остановка потоков является исключительно кооперативной.
Когда поток спит в Thread.sleep(), ждет на мониторе wait() или блокируется в очереди BlockingQueue.take(), вызов thread.interrupt() выбрасывает InterruptedException и сбрасывает флаг прерывания потока в false.
Правило обработки:
public void runWorkerLoop() {
while (!Thread.currentThread().isInterrupted()) {
try {
Task task = taskQueue.take(); // Бросает InterruptedException
task.execute();
} catch (InterruptedException ex) {
// ОБЯЗАТЕЛЬНО: восстановить прерванный статус потока!
Thread.currentThread().interrupt();
log.info("Worker thread interrupted, exiting gracefully...");
break; // Завершаем рабочий цикл
}
}
}
🎯 Шпаргалка для интервью
30-секундный ответ
- С точки зрения компилятора Java, обязательными к обработке являются только Checked Exceptions (
IOException,SQLException,InterruptedException). Компилятор требует либо обернуть их вtry-catch, либо задекларировать в секцииthrows.- С точки зрения архитектуры,
RuntimeExceptionобязательно перехватываются на границах системы (в@RestControllerAdvice, обработчиках очередей Kafka, фильтрах безопасности) для предотвращения падения потоков и формирования понятных ответов клиенту.- Системные ошибки
Errorперехватывать категорически запрещено: они свидетельствуют о физической неработоспособности JVM (OutOfMemoryError), и процесс должен быть перезапущен оркестратором.- На границах многопоточности обязательна установка
Thread.setDefaultUncaughtExceptionHandler.
4 каверзных вопроса с ответами
-
Что произойдет, если в задаче, переданной в
executorService.submit(runnable), произойдет деление на ноль (ArithmeticException)? Ответ: Пул потоков перехватитArithmeticExceptionи сохранит его внутри возвращенного объектаFuture. Никаких логов в консоль выведено не будет. Если вызывающий поток не вызоветfuture.get(), исключение будет бесследно проглочено. Если вызватьfuture.get(), оно будет выброшено обернутым вExecutionException. -
Почему перехват
catch (Throwable t)считается опасным антипаттерном? Ответ: КлассThrowableявляется общим предком дляExceptionиError. ПерехватываяThrowable, блокcatchзаблокирует естественное падение виртуальной машины приOutOfMemoryErrorилиStackOverflowError. Приложение останется в полуразрушенном «зомби»-состоянии с исчерпанной памятью или разрушенными локами. -
Что происходит с флагом прерывания потока при возникновении
InterruptedExceptionи как правильно поступить? Ответ: Механизм JVM при выбрасыванииInterruptedExceptionавтоматически сбрасывает флаг прерывания вfalse. Если разработчик просто залогирует ошибку и продолжит работу, внешний сигнал об остановке сервиса будет потерян. Обработчик обязан либо пробросить исключение дальше, либо восстановить статус вызовомThread.currentThread().interrupt(). -
Как компилятор Java проверяет обязательность обработки checked-исключений внутри многомодульных проектов? Ответ: На основе атрибута байткода
Exceptionsв.classфайлах зависимостей. Компиляторjavacстроит граф вызовов и проверяет, что каждый вызываемый метод с объявленнымthrowsлибо заключен в блокtryс соответствующимcatch, либо текущий метод сам содержит данное исключение (или его предка) в своемthrows.
Красные флаги (чего категорически нельзя говорить)
- ❌ «Нужно всегда оборачивать весь код метода в catch (Throwable t), чтобы приложение никогда не падало» (грубейшая ошибка: это ломает мониторинг и приводит к зомби-процессам при OutOfMemoryError).
- ❌ «Компилятор заставляет обрабатывать NullPointerException» (NPE — это unchecked исключение, компилятор его не контролирует).
- ❌ «Если поток прервали через InterruptedException, можно просто продолжить цикл» (нарушает кооперативную многопоточность и мешает graceful shutdown).
- ❌ «В микросервисах checked-исключения обязаны доходить прямо до фронтенда» (checked-исключения инфраструктуры обязаны транслироваться в доменные ошибки на границе слоев).