Статьи

Иерархия исключений в Java: что, зачем и как с этим жить

Когда код летит в стену, первым к тебе прилетает исключение. И если ты не понимаешь, кто его родственники, откуда оно взялось и чего от тебя хочет — отлаживать приложение превращается в охоту на привидений.

Java не просто так ввела чёткую иерархию исключений. Она помогает JVM грамотно обрабатывать ошибки, а тебе — структурировать код, не скатываясь в «try-catch всё подряд».

Часть 1. Полная карта иерархии исключений

Вот краткое дерево (для визуального представления):

Часть 2. Throwable и его особенности

У него есть два поля: detailMessage и cause. Они участвуют в трассировке стека.

В Throwable уже реализованы методы:

• getMessage()
• getCause()
• printStackTrace()

Ты можешь расширить Throwable напрямую, но это почти никогда не нужно. Лучше использовать Exception или RuntimeException.

Часть 3. Error — не твоя зона ответственности

Часто возникает вопрос: «А можно ли поймать Error?» — технически да. Практически — нет смысла.

• VirtualMachineError — признак краха JVM.

• OutOfMemoryError, StackOverflowError — чаще всего требуют пересмотра архитектуры или настройки JVM (а не оборачивания в try-catch).

• Их не логируют в обычном логе ошибок — для них выделяют отдельные метрики (например, алерты в APM-системах).

Часть 4. Checked vs Unchecked — где та грань?

Критерий
Checked Exception
Unchecked Exception
Компилятор требует обработки
Да
Нет
Наследуется от
Exception
RuntimeException
Когда применять
Внешние ресурсы, нестабильная среда
Ошибки в логике, контракте методов

Часть 5. Кастомные исключения: когда и как

Создавать свои исключения стоит, если:

• Тебе нужно чётко разграничить типы ошибок
• Ты хочешь сделать обработку понятнее и читаемее
• Стандартных классов недостаточно

Пример:
Не стоит:

• Создавать исключение ради одного if
• Наследовать Exception, если ты не собираешься заставлять всех его обрабатывать
• Пихать всю бизнес-логику в исключения

Часть 6. Обработка исключений по уму

Не надо так:
Это — классический антипаттерн. Почему?

e.printStackTrace() — это не логгирование. Ни уровня, ни контекста, ни нормального вывода в журнал, особенно если ты используешь нормальный логгер (а ты должен).

• Ловить просто Exception — значит ловить всё подряд, даже то, что ловить не должен (например, InterruptedException, IllegalStateException и другие, которые лучше пробрасывать).

• Блок try слишком большой (// много кода). Это плохо, потому что непонятно, где именно может возникнуть исключение. Лучше выделить минимальный участок кода, где реально может произойти ошибка.

Надо так:
Здесь всё красиво:

• Ловим конкретное исключение
• Даём понятный лог: что произошло, где, и почему это важно
• Делаем разумную реакцию (уведомляем пользователя)

И немного про multi-catch:
• Мы ловим несколько логически схожих исключений в одном блоке — удобно и читаемо.

• В логгировании помимо e.getMessage() мы передаём само исключение e как последний аргумент. Это важно: если понадобится трассировка стека (stack trace), она будет доступна в логах, без дополнительных действий.

Если резюмировать: пиши try-catch осознанно. Не оборачивай “на всякий случай”, не лови всё подряд и всегда задавай себе вопрос: "А что я собираюсь делать, если здесь правда случится ошибка?"

Часть 7. Проброс исключений (throws) — способ не захламлять код

Как мы уже говорили в части 4, checked-исключения требуют обязательной обработки. Компилятор не даст тебе пройти мимо IOException, SQLException и им подобных — ты обязан либо перехватить их, либо явно сказать: «Я не хочу с этим разбираться здесь, пусть разберутся выше».

Посмотри:
Такой код не скомпилируется, если readFile() выбрасывает IOException. Ты должен сделать одно из двух:

Вариант 1 — обработать прямо здесь:
Вариант 2 — пробросить дальше:
Этот второй вариант — и есть проброс исключения. Ты как бы говоришь вызывающему методу: «Я знаю, что тут может быть ошибка, но обрабатывать её должен ты».


Когда использовать throws, а не try-catch?

Если ты пишешь код в глубоком слое бизнес-логики, то есть смысл не захламлять его лишней обработкой исключений. Вместо этого пробрось ошибку наружу, а там, ближе к пользователю (например, в контроллере или сервисе верхнего уровня) уже разберись, как на неё реагировать: показать ошибку, залогировать, вернуть код ответа и т.д.

Пример:
Главное — не прятать ошибки, а передавать ответственность

throws — не способ избежать исключений, а инструмент, чтобы не решать проблему там, где ты не можешь решить её качественно. Это особенно полезно в больших проектах, где каждый слой отвечает только за свою часть логики.

Часть 8. Исключения в функциональном коде (Stream, lambda)

Сложная тема — в лямбдах нельзя бросать checked exceptions напрямую.
Варианты:

• Обернуть в RuntimeException
• Использовать вспомогательные библиотеки (например, Vavr)
• Делать врапперы с try-catch внутри

Часть 9. Исключения и архитектура: где место?

• Не кидай исключения там, где можно вернуть нормальный результат (например, Optional или Result-like объект)
• Используй исключения, чтобы прервать выполнение, когда это реально исключительная ситуация
• В REST API пробрасывай бизнес-ошибки до слоя контроллеров и возвращай понятный клиенту ответ (400, 404, 409, а не 500 на всё подряд)

Иерархия исключений в Java — это не абстрактная конструкция, а инструмент. Когда ты понимаешь её принципы, писать надёжный, чистый и понятный код становится проще. Да, можно обойтись RuntimeException и жить как будто всё хорошо — но только до тех пор, пока приложение не упадёт на проде, а лог покажет «NullPointerException at line 42». А дальше — боль, кофе и глубокое погружение в стек.


Хотите узнать больше? Изучите другие статьи из раздела:
Java