# Exceptions Handling

> "Каждый проект должен сформулировать, согласовать и соблюдать внутри команды разработки методику и принципы работы с исключениями, в противном случае проект обречен утопать в бесконечных SPI/LSR"

## Элементарные принципы

**Не перехватывать Throwable**, для этого не существует конструктивного сценария обработки.

**Не перехватывать Error и подклассы**, кроме особых случаев, когда это ожидаемая ошибка и ```Error``` специфицированный. Например, ```LinkageError``` при загрузке пользовательского плагина в рантайме.

**Не перехватывать Exception**, если только это вынужденная мера, когда вызываемый метод его декларирует и невозможно изменить сигнатуру вызываемого метода.

**Не декларировать ни Throwable, ни Error, ни Exception, ни RuntimeException** в объявлении метода.

**Использовать NullPointerException только для сигнализации об ошибке разработчика**. Единственное назначение ```NullPointerException``` - сигнализация об ошибке разработчика. В некоторых руководствах можно встретить рекомендацию о том, чтобы выбрасывать ```NullPointerException``` при проверке аргументов на ```null```. Такую рекомендацию следует трактовать так: если проверка является инвариантом, утверждением, пред/пост условием, то можно использовать методы ```Preconditions::checkNotNull```, ```Objects::requireNonNull``` и подобные. Однако, стоит при этом понимать, требуется ли вызывающему коду, при обработке исключения, различать ```NullPointerException``` как результат проверки инварианта/утверждения/условия, и как внутреннюю ошибку в вызываемой функции. В публичных контрактах использование ```NullPointerException``` для проверки аргументов не рекомендуется, потому что не позволяет различать ошибку в коде вызова и внутреннюю ошибку вызываемого компонента, соответственно, невозможно, например, настроить мониторинг так, чтобы различать клиентские ошибки и ошибки компонента.

**Декларировать и выбрасывать только специфицированные подклассы Exception, RuntimeException, Error**. Допускается выбрасывать не специфицированный ```RuntimeException```, но не в публичном контракте.

{% cut "Почему не специфицированные исключения не рекомендуется использовать" %}

Не специфицированное исключение - это слишком широкий класс ошибок. Использование не специфицированных исключений затрудняет эксплуатацию, обработку и анализ ошибок.

**Эксплуатация**

Если у сервиса есть только один класс исключений, то, что бы ни случилось, нужно всегда реагировать на алерт мониторинга, т.е. идти смотреть, что именно случилось. Если ошибки разделить на классы, то мониторинг можно настроить согласно критичности класса ошибок.

**Обработка ошибок**

Как можно обработать ошибку, которая формулируется как "что-то случилось!"? Никак. Только залогировать и показать пользователю ошибку "у нас что-то случилось". У не специфицированного исключения меньше степеней свободы для транспорта деталей ошибки, - только сообщение и стектрейс (у специфицированного исключения дополнительно есть иерархия, тип, данные). Определять ошибку по сообщению или стектрейсу очевидно bad practice.

**Анализ ошибок**

Если ошибки/исключения не классифицируются, то их трудно структурировано анализировать. 

{% endcut %}

**Пустой catch блок - это плохой подход**. Иногда так поступают с checked исключениями, которые "никогда" не должны произойти. Такое исключение рекомендуется, как минимум, логировать.

## Структурирование

**При создании собственной иерархии исключений, обеспечить ее расширяемость через подклассы базового специфицированного исключения**. Если у каждого терминального исключения в иерархии будет специфицированный базовый тип, то такое исключение не будет пропущено клиентским кодом (должен обрабатывать, как минимум, базовый тип исключения), даже если клиент не обрабатывает его явно.

**Если требуется иерархия исключений, то использовать типовую триаду ```client-service-internal``` классификации ошибок, которая подходит для большинства компонентов и сервисов.** Примером такой триады является иерархия исключений публичных методов ```RestTemplate``` с ```RestClientException``` в качестве базового класса. В остальных случаях, ориентироваться на нужды вызывающего кода и на то, как будет выглядеть код обработки иерархии исключений.

{% cut "Что такое client-service-internal?" %}

Любые ошибки взаимодействия между программными (компоненты в самом широком смысле, – функция, класс, модуль, подсистема, сервис и т.д.) можно классифицировать в 3 типа ошибок:

* **Client**. Клиентские ошибки. Когда компонент А вызвал компонент В с такими параметрами, которые определяются компонентом В как некорректный запрос.
* **Service**. Ошибки обслуживания. Когда компонент А вызвал компонент В, но компонент В не может завершить обслуживание компонента А по причине, не зависящей от компонента А, но известной компоненту В.
* **Internal**. Внутренние/неизвестные ошибки обслуживания. Когда компонент А вызвал компонент В, но компонент В не может завершить обслуживание компонента А по причине, не зависящей от компонента А и не известной компоненту В. 

{% endcut %}

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

{% cut "пример с встроенным типом ошибки" %}

```java
public enum ServiceError {
   BAD_THINGS_HAPPEN, 
   OTHER_BAD_THINGS_HAPPEN, 
   // ... и т.д., например, более десятка
}

if(/* bad thing condition */) {
   throw new ServiceException(ServiceError.BAD_THINGS_HAPPEN);
}
```

{% endcut %}

**Если обработка исключений совмещается с сложной логикой мониторинга, аудита, аналитики и т.д., то использовать структурированное инструментирование**. Пример: [domain-oriented-observability](https://martinfowler.com/articles/domain-oriented-observability.html)

## Создание, обработка и распространение исключений

**Рекомендуется использовать unchecked исключения в общем случае, а checked в специальных случаях**. Checked исключения - это единственный способ, с помощью которого можно гарантировать, что вызывающий код(клиент) возьмет на себя ответственность за обработку исключения. Checked исключение рекомендуется использовать, когда ожидается или декларируется, что клиент способен и должен обрабатывать такие исключения. InterruptedException - хороший пример checked исключения. 

**Возвращать, эскалировать ошибки и их контекст только с помощью исключений**. Коды ошибок можно использовать только в особых случаях, например, когда требуется высокая производительность, а частота и/или стоимость возбуждения исключения слишком высока. Однако, иногда трудно отнести исключительную ситуацию к ошибке, например, в случае валидации данных. При валидации бывает предпочтительнее возвращать объект-контейнер ошибок. Также, монады часто являются "правильной" альтернативой при выборе между монадами и исключениями.

**Логировать ошибки и исключения так, чтобы при разборе в логе было достаточно информации для анализа и исправления ошибки**. 

**Совмещение логирования и выбрасывания исключения, в общем случае, не рекомендуется**. Это необходимо, чтобы избежать дублирования записей в логах, не эффективного использования логирования, усложнения анализа логов эксплуатацией и разработкой. Однако, если контекст ошибки важен и существует только в месте ее возникновения, а обработка выше по стеку, то контекст либо должен стать частью состояния исключения, либо быть залогирован (при невозможности, нецелесообразности или запрете на передачу контекста ошибки через исключение).

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

**При передаче контекста ошибки через исключения, не раскрывать внутреннюю реализацию и не нарушать инкапсуляцию объектов, данные или состояние которых передается исключениями**.

**Не использовать исключения для управления потоком исполнения, только для ошибок и исключительных ситуаций**.

**Выбрасываемые исключения должны соответствовать текущему слою абстракции**.

**Стоит выбрасывать исключение как можно раньше и ближе к источнику возникновения ошибки**.

**Не пробрасывать не специфицированные исключения**. Использовать re-mapping/wrapping.

{% cut "Re-mapping" %}

Когда вместо обработки исключения А выбрасывается исключение Б.

{% endcut %}

{% cut "Wrapping" %}

Когда вместо обработки исключения А выбрасывается исключение Б, которое оборачивает исключение А, опционально изменяет, дополняет сообщение и контекст ошибки.

{% endcut %}

**Не терять оригинальное исключение при wrapping'е**.

**Не использовать исключения стандартной библиотеки для публичных контрактов сервисов**. Этот принцип не относится к реализации компонентов уровня стандартной библиотеки или модуля.

**Не генерировать исключения в конструкторе, если это возможно**. В противном случае, необходимо понимать, что при возникновении исключения память под объект будет выделена в куче, а также объект может быть частично инициализирован, что может приводить к потенциальным проблемам в безопасности(finalizer attack) и проблемам в многопоточной среде.

{% cut "А как же валидация в конструкторе при создании immutable-объекта?" %}

Так делать можно:

```java
public record Rectangle(int width, int height) {
    public Rectangle {
        Preconditions.checkArgument(width > 0, "Width must be nonzero");
        Preconditions.checkArgument(height > 0, "Height must be nonzero");        
    }
}
```

Альтернативный подход - делегирование проверок в static factory method или factory.

{% endcut %}

**Исключения должны быть иммутабельными объектами**. Исключение - это snapshot состояния системы во время возникновения ошибки.

**Не использовать getCause() и instance of в обработчиках**. Это приводит к созданию "хрупкого" кода и ухудшает читабельность.

##  Работа с ресурсами

**Корректно обрабатывать запрос на остановку потока и InterruptedException**. Корректная обработка означает, что запрос на остановку (```Thread::interrupt```) не утрачивается в стеке вызовов потока и доходит до соответствующего обработчика. Заранее неизвестно, в каком фрейме будет находиться исполнение программы, когда придет запрос на остановку потока. Сигнал о запросе остановки передается через флаг потока _INTERRUPT_. Если метод выполняется быстро (это определяется нефункциональными требованиями), то, очевидно, можно не проверять этот флаг, но сохранять его нужно **всегда** (кроме кода, в котором фактически и окончательно происходит обработка запроса на остановку потока). Если метод выполняется долго (также определяется нефункциональными требованиями), то необходимо проверять флаг _INTERRUPT_, чтобы либо завершить работу потока, либо передать запрос об остановке по стеку.

Флаг _INTERRUPT_ не будет выставлен для потока, если поток находился в заблокированном состоянии в то время, когда был вызван ```Thread::interrupt```. Вместо этого будет выброшено исключение (```InterruptedException```, ```ClosedByInterruptException``` и др.). Если обработчик запроса остановки потока находится не в текущем фрейме, то при обработке checked исключения, такого как ```InterruptedException```, необходимо восстановить флаг _INTERRUPT_ с помощью вызова ```Thread.currentThread().interrupt()```.

Если прерываемая функция возвращает значение, то, руководствуясь требованиями к системе, разработчик решает, следует выбросить исключение или вернуть какое-то значение. Для корректной обработки запроса на остановку потока важно только, чтобы запрос был, в конечном итоге, обработан за требуемое время, определяющееся нефункциональной спецификацией.

Важно понимать, что передача сигнала о запросе остановки потока (от фрейма, в котором обнаружен запрос до обработчика) делается двумя взаимоисключающими способами: 

* ```Thread.currentThread().interrupt()```
* пробросить ```InterruptedException``` или другое _checked_ исключение, однозначно трактующееся вызывающим кодом, как запрос об остановке, возникший во время работы вызываемого метода

**Механизм запроса остановки потока в Java и сигналы в POSIX и др. системах - это разные механизмы**. То есть, SIGINT(Ctrl-C) не создает запроса на остановку потоков в JVM, если только это не сделано явно разработчиком в shutdown hook'е.


**Защищаться от утечки ресурсов с помощью  ```try-with-resource```**.

**При использовании catch в ```try-with-resource``` не игнорировать перехваченные исключения**. Исключения, возникающие при закрытии ресурсов, также будут проигнорированы.

**При использовании finally в ```try-with-resource``` явно обработать исключения, которые могут возникнуть внутри finally**. В противном случае, исключения из finally блока перетрут исключения из try и catch блоков, то есть не будут добавлены в supressed. 

**При реализации Closable интерфейса, сделайте метод close идемпотентным.**

**Не использовать выход из finally блока через return оператор**. Это может привести к потере исключения.

{% cut "пример: исключение будет потеряно" %}

```java
try {
   throw new IllegalStateException();
} finally {
   return false;
}
```

{% endcut %}

**При обработке исключений внутри finally позаботиться о том, чтобы возникшее при этом исключение не приводило к утечке ресурсов**. 

{% cut "пример" %}

```java
try {
   // ...
} finally {
   resource.free(); // RuntimeException может сделать недостижимым освобождение otherResource
   otherResource.free();
}

try {
   try {
      // ...
   } finally {
      resource.free(); // throws ResourceFreeException
      otherResource.free(); // Недостижимо в случае ResourceFreeException у resource::free
   }
} catch(ResourceFreeException e) {
   // ...
}
```

{% endcut %}

**При разработке ресурсов, компонентов и структур данных стремиться к тому, чтобы, при возникновении исключения, ресурс, компонент, структура данных оставались в состоянии предшествующем отказу, то есть обладали //failure atomic// свойством.**

## Проектирование

**Не обработанные исключения не должны протекать через публичный контракт сервиса**. Это строгое требование к соблюдению для конечного сервиса, обслуживающего пользователей или другие сервисы. Для компонента, являющегося составной частью сервиса, это рекомендация. В сервисе должна быть предусмотрена централизованная обработка исключений, не являющихся частью публичного контракта, и для которых не нашлось обработчика внутри сервиса. Такие исключения должны регистрироваться и быть предметом разбора, в результате которого их обработка перемещается внутрь сервиса, или они становятся частью публичного контракта.

**Исчерпывающе документировать публичный контракт**. Как минимум, все явно возбуждаемые исключения, checked и unchecked, должны быть подробно специфицированы.

**Исключения уровня публичного контракта должны четко определять гарантии о состоянии сервиса после возникновения исключения**. См. [Exception safety](https://en.wikipedia.org/wiki/Exception_safety)

**Осознанно использовать стандартные подходы к конвертации исключений в http коды**. Это не тривиальный вопрос, требующий проектирования и принятия осознанного архитектурного решения.  Http протокол - это транспорт/инфраструктура для сервиса, поэтому, в общем случае, не должен использоваться для представления прикладных/бизнес ошибок. Прикладные ошибки должны быть _инкапсулированы_ в рамках этого протокола. Рекомендуется использовать ошибки http протокола при реализации инфраструктурных задач (напр., кэширование, аутентификация и тд.), или когда http является способом доступа к сервису (напр., WebDav как способ доступа к файловой системе, к репозиторию каких-либо объектов).

{% cut "Пример проблемы, требующей архитектурного решения" %}

Сервис разворачивается в Qloud, доступ к сервису осуществляется через балансер. Балансер - это полноправный участник http коммуникации. Поэтому может потребоваться решение, позволяющее отличать отказы балансера от отказов инстансов сервиса. Если сервис широко использует http коды, то для клиента это потенциально представляется неопределенностью требуемых действий, потому что семантика http кодов у балансера и сервиса, с большой вероятностью, будет отличаться.

{% endcut %}

{% cut "Проблема редукции прикладных ошибок" %}

Несмотря на то, что http кодов относительно много, их, как правило, не достаточно для отображения множества прикладных ошибок комплексного сервиса

{% endcut %}

{% cut "Проблема связки MVC фреймворка и Servlet контейнера" %}

Обработка исключений не может быть ограничена и решена только в рамках MVC фреймворка. Servlet контейнер - это обязательный участник протокола передачи ошибок, который формулируется сервисом. 

Например, если, в случае ошибки, передается json в теле ответа, то этот же формат ошибок нужно поддержать и для ошибок уровня servlet контейнера, то есть для тех ошибок, которые возникнут в servlet контейнере и останутся не заметны для диспетчера MVC фреймворка. В случае использования Jetty см. [ErrorHandler](https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/server/handler/ErrorHandler.html)

{% endcut %}

## Ссылки:
* [Joshua Bloch, "Effective Java"](https://lib.yandex-team.ru/books/5376)
* [Bjarne Stroustrup, "Exception Safety"](http://www.stroustrup.com/except.pdf)
* [Robert C. Martin, "Clean Code"](https://lib.yandex-team.ru/books/2156)
