Источник Обработка ошибок в любой разработке играет важнейшую роль. В программе может пойти не так практически всё: пользователь введёт некорректные данные, или они могут прийти такими по http, или мы ошиблись при написании сериализации/десериализации и в процессе обработки программа падает с ошибкой. Да может банально закончится место на диске.  ¯_(ツ)_/¯, нет единого способа, и в каждой конкретной ситуации придётся подбирать наиболее подходящий вариант, но есть рекомендации, как это делать лучше. К сожалению (или просто такая жизнь?), этот список можно продолжать бесконечно. Разработчику постоянно нужно думать о том, что где-то может возникнуть ошибка, и тут есть 2 ситуации: И если ожидаемые ошибки хотя бы локализованы, то остальные могут произойти практически везде. В случае, если мы не обрабатываем ничего важного, то можно просто упасть с ошибкой (хотя и такое поведение недостаточно и требуется как минимум добавить сообщение в лог об ошибке). Но если именно сейчас происходит обработка платежа и нельзя просто упасть, а нужно хотя бы вернуть ответ о неуспешной операции?  Перед тем как рассмотрим способы обработки ошибок, несколько слов об Exception (исключениях): Источник Иерархия исключений хорошо описана и о ней можно найти много информации, поэтому нет смысла тут её расписывать. Что до сих пор иногда вызывает жаркое обсуждение, так это checked и unchecked ошибки. И хоть unchecked исключения большинство приняло предпочтительными (в Kotlin вообще нет checked исключений), с этим не все ещё согласны.  За checked исключениями действительно стояло благое намерение сделать их удобным механизмом обработки ошибок, но реальность внесла свои корректировки, хоть и сама идея внесения в сигнатуру всех исключений, которые могут быть брошены из этой функции, понятна и логична. Давайте рассмотрим это на примере. Предположим, у нас есть функция method, которая может бросить проверяемое исключение PanicException. Такая функция будет выглядеть следующим образом: Из её описания видно, что она может бросить исключение и что исключение может быть только одно. Вроде выглядит вполне удобным? И пока у нас маленькая программа, всё так и есть. Но если программа чуть больше и таких функций становится больше, то появляются некоторые проблемы.  Проверяемые исключения требуют по спецификации, чтобы в сигнатуре функции перечислялись все возможные проверяемые исключения (либо общий предок для них). Поэтому, если у нас есть цепочка вызовов a -> b -> c и самая вложенная функция кидает какое-либо исключение, то оно должно по цепочке быть проставлено у всех. А если этих исключений несколько, то и у самой верхней функции в сигнатуре должно быть описание их всех.  Так, по мере усложнения программы, этот подход приводит к тому, что у верхней функции исключения постепенно схлопываются к общим предкам и сводятся в конечном счёте к Exception. Что в таком виде становится похожим на unchecked исключение и сводит на нет все преимущества проверяемых исключений. А если учесть, что программа, как живой организм, постоянно изменяется и эволюционирует, то практически невозможно заранее предусмотреть, какие исключения могут в ней возникать. И в результате получается ситуация, что когда мы добавляем новую функцию с новым исключением, приходится пройтись по всей цепочке её использования и менять сигнатуры у всех функций. Согласитесь, это не самое приятное занятие (даже учитывая, что современные IDE это делают за нас).  Но последний, и, наверное, самых большой гвоздь в проверяемые исключения «вогнали» лямбды из Java 8. В их сигнатуре нет никаких проверяемых исключений ¯_(ツ)_/¯ (т.к. в лямбде можно вызывать любую функцию, с любой сигнатурой), поэтому любой вызов функции с проверяемым исключением из лямбды заставляет оборачивать её в проброс исключения как непроверяемое: К счастью, в спецификации JVM вообще нет проверяемых исключений, поэтому в Kotlin можно в такой же лямбде ничего не оборачивать, а просто вызывать нужную функцию.  Хоть это и приводит иногда к неожиданным последствиям, как, например, к неверной работе @Transactional в Spring Framework, который «ожидает» только unckecked исключения. Но это больше особенность фреймворка, и, возможно, такое поведение в Spring изменится в ближайшее время github issue. Исключения сами по себе являются особыми объектами. Помимо того, что их можно «пробрасывать» через методы, они ещё и собирают stacktrace при создании. Эта особенность потом помогает с анализом проблем и поиском ошибок, но может и привести к некоторым проблемам с производительностью, если логика работы приложения становится сильно завязанной на бросаемые исключения. Как показано в статье, отключение сборки stacktrace позволяет в этом случае значительно увеличить их производительность, но к нему стоит прибегать только в исключительных случаях, когда это действительно требуется! Основное, что нужно сделать с «неожиданными» ошибками, — найти место, где можно их перехватить. В JVM-языках это может быть либо точка создания потока, либо фильтр/точка входа в http-метод, где можно поставить try-catch с обработкой unchecked ошибок. Если вы используете какой-либо фреймворк, то, скорее всего, в нём уже есть возможность создавать общие обработчики ошибок, как, например, в Spring Framework можно использовать методы с аннотацией @ExceptionHandler. До этих же центральных точек обработки можно «поднимать» исключения, которые мы не хотим обрабатывать в конкретных местах, прокидывая те же unckecked исключения (когда, например, не знаем, что делать именно в конкретном месте и как обрабатывать ошибку). Но этот способ не всегда подходит, потому что иногда может потребовать обработать ошибку на месте, и нужно проверять, что все места вызовов функций правильно обрабатываются. Рассмотрим способы сделать это. Всё же использовать исключения и тот же try-catch: Основной недостаток в том, что мы можем «забыть» обернуть его в try-catch в месте вызова и пропустить попытку обработки на месте, из-за чего исключение пробросится наверх до общей точки обработки ошибки. Тут можно перейти к checked исключениям (для Java), но тогда мы получим все те недостатки, о которых упоминалось выше. Этот подход удобно использовать, если обработка ошибки на месте не всегда требуется, но в редком случае она нужна.  Использовать sealed class как результат вызова (Kotlin).
В Kotlin можно ограничить количество наследников у класса, сделать их вычисляемыми на этапе компиляции — это позволяет компилятору проверять, что все возможные варианты будут разобраны в коде. В Java можно сделать общий интерфейс и несколько наследников, правда, теряя проверки на уровне компиляции. Тут мы получаем что-то вроде golang-подхода к ошибкам, когда нужно в явном виде проверять результирующие значения (или явно игнорировать). Подход достаточно практичный и особенно удобный, когда требуется в каждой из ситуаций прокидывать много параметров. Класс Result можно расширить различными методами, которые упрощают получение результата с пробросом исключения выше, если таковое есть (т.е. нам не нужно в месте вызова обрабатывать ошибку). Основным недостатком будет только создание промежуточных лишних объектов (и чуть более многословная запись), но и его можно убрать, используя inline классы (если нам достаточно одного аргумента). и, как частный пример, есть класс Result из Kotlin. Правда, он пока только для внутреннего использования, т.к. в будущем его реализация может немного измениться, но если хочется им воспользоваться, то можно добавить флаг компиляции -Xallow-result-return-type. Как один из возможных видов п.2, использование типа из функционального программирования Either, который может быть либо результатом, либо ошибкой. Сам тип может быть как sealed классом, так и inline классом. Ниже пример использования реализации из библиотеки arrow: Больше всего Either подойдёт тем, кто любит функциональный подход и кому по душе строить цепочки вызовов.  Использовать Option или nullable тип из Kotlin: Такой подход подойдёт, если не очень важна причина ошибки и когда она только одна. Пустой ответ считается ошибкой и пробрасывается выше. Самая короткая запись, без создания дополнительных объектов, но такой подход не всегда можно применить.  Аналогичен п.4, только использует хардкодное значение как маркер ошибки: Наверное, это самый старый подход к обработке ошибок, пришедший ещё из C (или даже с Algol). Никаких накладных расходов, только не совсем понятный код (вместе с ограничениями на выбор результата), но, в отличие от п.4, появляется возможность делать различные коды ошибок, если требуется больше одного возможного исключения.  Все подходы можно комбинировать в зависимости от ситуации, и нет среди них того, который подойдёт во всех случаях.  Так, например, можно добиться подхода golang к ошибкам, используя sealed классы, а там, где это не очень удобно, переходить к unchecked ошибкам.  Или использовать в большей части мест nullable-тип как маркер того, что не удалось подсчитать значение или достать его откуда-либо (например, как индикатор, что значение не нашлось в базе). А если же у вас полностью функциональный код вместе с arrow или ещё какой-либо аналогичной библиотекой, то тогда, скорее всего, лучше использовать Either.  Что же до http-серверов, то в них проще всего поднимать все ошибки до центральных точек и только в некоторых местах комбинировать nullable подход с sealed классами.  Буду рад увидеть в комментариях, что из этого используете вы, а может, есть ещё другие удобные методы обработки ошибок? И спасибо всем, кто дочитал до конца! Разработчик