Во многих программах написанных на Kotlin, в том числе и доступных на GitHub, а также в некоторых статьях, особенно на Medium, я снова и снова вижу, что авторы обрабатывают ошибки, с моей точки зрения, неэффективно и неидиоматически. При этом результат работы программы может быть правильный. Неэффективность и «неидеоматичность» проявляется в этих случаях в том, что авторы либо используют дополнительные классы, либо городят ненужные костыли, либо делают код более сложным для чтения, понимания и поддержки, чем нужно.  И наш новый друг, ChatGPT, по крайней мере в моих экспериментах, видимо начитавшись плохих примеров, предлагает очень странные решения. Не претендуя на истину в последней инстанции, я попытаюсь в этой статье описать систематический подход к оптимальной и идиоматической обработке ошибок в Kotlin.  Эта публикация должна быть интересна в первую очередь новичкам в Kotlin. Но я надеюсь, она будет небезинтересна и профессионалам.  Полностью исходные коды к этой статье вы найдёте здесь.  Должен сознаться, что приведённые выше нарекания касаются и некоторых моих собственных старых программ на Kotlin.  Думаю, причины проблем многих программистов, не‑идиоматически обрабатывающих ошибки на Kotlin те же, что и у меня: Одно неправильное наименование одного параметра в документации, которое «отпугивает» многих пользователей, как это было и со мной (об этом — ниже), Излишний лаконизм документации Kotlin, особенно — отсутствие описания мотивации и примеров использования, Попытки новичков в Kotlin применить привычные паттерны из других языков программирования вместо изучения и последующего использования базовых средств Kotlin. Рассмотрим для простоты некоторую функцию, реализованную на некотором (пока не важно каком) языке программирования, которая вычисляет или меняет какое‑то значение. Если при исполнении кода этой функции произошла ошибка, то как правило, дальнейшая логика исполнения должна измениться по сравнению с нормальным случаем исполнения. Для этого надо как‑то известить «внешний мир» о произошедшей ошибке.  Лично мне известны следующие способы такого извещения, бывшие популярными в различных языках программирования в разные времена: Записать информацию об ошибке в какую‑то внешнюю (как правило — глобальную) переменную или объект.  Использовать специальную языковую конструкцию исключения ( Exception), куда записывается информация об ошибке и которая, прерывая нормальную логику исполнения, начинает двигаться вверх по стеку вызовов до места, где она встретиться со своим перехватчиком.  В каждой функции иметь специальный выходной параметре, в который записывается информация об ошибке. Суметь в качестве возвращаемого параметра выдавать либо вычисленный результат, либо информацию об ошибке. Каждый из перечисленных подходов имеет свои преимущества и недостатки. Поговорим о недостатках. В первом и и третьем случае после вызова каждой функции надо соответствующую переменную или объект проверять и потом вычищать. Это делает код «грязным».Использование механизма Exception (второй подход) — базовый механизм обработки ошибок в Java. Интернет запружен стонами Java‑программистов на эту тему. Не вдаваясь в детали, приведу две публикации на эту тему: Checked Exceptions are Evil и Why are exceptions considered bad in many programming languages when they make your code cleaner?  Первые два подхода грешат также сторонними эффектами. Другими словами, состояние окружение точки вызова нашей условной функции в случае нормального её выполнения отличается от состояния, если в ней произошла ошибка. А это — причина следующих коварных ошибок, которые очень трудно отловить.  Третий подход такой проблемы не создаёт, но очень раздражает наличием дополнительного, необъяснимого с точки бизнес‑логики параметра.  В результате для многих практиков и теоретиков программирования наиболее приемлемым на сегодня представляется четвёртый подход.  Таким образом, нам надо исхитриться в возвращаемое значение функции суметь вставлять либо результат нормального вычисления, либо информацию об ошибке. Как это можно сделать в Java с помощью класса Result, я описал несколько лет назад в моей статье. В той статье я сетовал, что пришлось этот класс разрабатывать самому, поскольку он отсутствует в стандартной поставке Java. Хорошая новость состоит в том, что в стандарте языка Kotlin такой класс есть и он тоже называется Result. При исследовании описания функций класса Result мне бросилось в глаза обстоятельство, изначально оттолкнувшее меня от использования этого класса. Сигнатура (включая имена) функции задания информации об ошибке выглядит так: Если верить имени входного параметра, то мы можем использовать эту функцию класса только с Exception, а не с нашими собственными ошибками типа ошибок валидирования входных параметров.  Но — не будем торопиться и разберёмся повнимательнее. Прочитав описание класса Throwablel мы увидим там член класса для описания ошибки message и причины ошибки — cause. Оба члена опциональны. Кроме того имеется возможность прочитать или напечатать стэк вызова.  Важная информация содержится в конце описания класса: Inheritors: open class Error : Throwable open class Exception : Throwable Лично я интерпретирую это так: разработчики языка используют Exception для упаковки информации о системных ошибках и предлагают одновременно остальные ошибки паковать в класс Error или его наследников. При этом наследники обеих веток наследованная будут одинаково обрабатываться в функции failure(...) класса Result и, разумеется, во всех других его функциях.  Значит, мы можем использовать класс Result при работе с собственными классами ошибок! Ну что же, попробуем использовать наши знания. Из моей многолетней практики программирования приложений в «кровавом энтерпрайзе» я знаю, что кроме сообщения об ошибке очень полезно ввести код или категорию ошибки а также некоторую информацию о деталях (например — некоторых значениях соседних переменных или состоянии окружения). Итак, создадим наш класс ошибки: А вот интересно, при задании инстанции (экземпляра) этого класса, будет ли автоматически внутри него задан стэк вызова? Ведь он может быть так полезен при разборе ошибочных ситуаций! Проверяем (полный текст теста здесь): Ура! Создаётся! Перейдём теперь к систематическому рассмотрению возможностей класса Result.  Наше систематическое изучение функций этого класса мы начнём с того, что попытаемся понять, а что мы хотим от подобного класса? Итак, предположим мы собираемся запрограммировать функцию на Kotlin, которая вычисляет и возвращает некоторое значение. При вычислении может произойти некоторая ошибка. (Пример функции, не возвращающей значение, мы рассмотри позже). Думаю, нам хотелось бы мочь:  в теле функции удобно задавать нормальный или ошибочный результат, в коде вызова функции удобно: Узнавать, проработала она нормально или с с ошибкой?  Получать нормальный либо ошибочный результат для дальнейшего использования либо  Проводить обработку обеих ситуаций. Функции класса Result, позволяющие задавать результат внутри некоторой функции и определять его «начинку» в вызывающей функции приведены в следующей таблице: Остальные функции класса посвящены обработке упакованного в инстанцию класса результата (нормального либо ошибочного - в виде информации об ошибке).  Эти функции в целом покрывают большинство мысленных потребностей такой обработки. Эти потребности выписаны в первом столбец таблицы внизу.  Если вы ищете функцию, удовлетворяющую эту потребность, вы должны в соответствующем ей столбце (в котором стоит указатель “¬") подобрать кандидатов, имеющих в соответствующей клетке в нижней части таблицы “Y”. Далее мы рассмотрим характерные примеры обработки ошибок с помощью класса Result. В Kotlin есть унаследованная от Java функция-расширение String, вычисляющая целочисленное представление строки. Вызывается эта функция так: Если исходная строка не является текстовым представлением целого числа, то функция выбрасывает Exception. Давайте создадим функцию, которая так не безобразничает, а выдаёт в качестве результата либо целое число, либо информацию об исключении. А дальше на примере этой функции рассмотрим правила использования класса Result. Новички в Kotlin, пришедшие из Java, напишут эту функцию наверное примерно так: Однако в Kotlin есть функция runCatching(...), которая позволяет сделать тоже самое элегантнее: Проверяем, действительно ли результат одинаков и заодно посмотрим, как работают функции, перечисленные в первой таблице вверху. В этом примере мы не только рассмотрели, как действуют функции из первой таблицы, но и использовали также функции “прямой” распаковки результата с помощью функций getOrNull(...) и exceptionOrNull(...). Очень часто бывает так, что любой ошибочный результат мы можем заменить неким стандартным значением. В этом случае наш лучший друг - функция  getOrDefault(…) Если в случае ошибки мы всегда знаем как задать в ошибочной ситуации новое значение, но логика эта не тривиальная, мы используем функцию getOrElse(…) Один из самых распространённых сценариев состоит в том, что в случае ошибки мы выбрасываем исключение, а нормальный результат обрабатываем дальше. В этом случае мы используем getOrThrow(...) Мы видим, что подход с запаковываем нормальных и ошибочных результатов в инстанцию класса Result пока неплохо работает. Досадная неприятность - на выходе результат нам надо распаковывать, чтобы использовать дальше. Получается, что мы сэкономили в одном месте и платим за это в другом. Это верно только отчасти. Класс Result предоставляет несколько удобных функций, которые позволяют трансформировать одно или оба (нормальное и ошибочное) значения без предварительной распаковки. Первая такая функция map(..), которая позволяет трансформировать нормальный результат и оставляет ошибочный без изменения. Для демонстрации её возможностей мы создадим функцию, использующую нашу функцию toIntSafe(…) и увеличивающее в нормальном случае значение на 1 без предварительной распаковки.  Проверим, как она работает: Если мы опасаемся, что в процессе трансформации может произойти ошибка, мы должны использовать функцию mapCatching(…). В этом случае в конце нам надо разбираться, произошла ли ошибка и если да, то где - до использования функции map(...) или в ней внутри.  Для демонстрации подхода мы создадим ещё одну функцию, складывающую значения, заданные в текстовом виде: Проверяем: Функции recover(…) и recoverCatching(…) работают аналогично map(…) mapCatching(…) но используют при этом ошибочное значение. Это может быть полезно, если вы по информации об ошибке можете задать новое правильное значение. Функция fold(…) позволяет объединить обе рассмотренные функции вызывать вместе, в одном вызове.  Чтобы повысить читабельность вашей реализации, рекомендуется использовать имена параметров onSuccess и onFailure. Проверяем: Того же результата можно добиться путём последовательного вызова двух функций onSuccess(…) и onFailure(…). Шарм подхода проявляется в том, что результат не зависит от того, в какой последовательности будут вызваны эти функции. В рассмотренных выше примерах мы рассмотрели использование класса Result в функциях, возвращающих результат. А что делать, если с точки зрения бизнес‑логики функция никакого результата не возвращает? Например, запись в базу данных или посылка данных по коммуникационному каналу как правило заканчивается хорошо, но иногда сбоит. Как обрабатывать ошибки без бросания Exception в этом случае? Один из хороших подходов — сделать функцию возражающей опционально информацию об ошибке, как в примере внизу. Но сначала определим две вспомогательные функции, которые нам потребуются в последующих тестах: Ну а теперь модифицированная "безрезультатная" функция и тест к ней: Иногда встречаются ситуации, когда сначала надо проверить, применима ли данная функция к данному набору параметров и только после этого её применять. Привет этом, например при парсинге или валидации данных, эта проверка и собственно применение функции пересекаются по коду и проверочная часть существенно «дороже» остальных действий. В этом случае мы можем говорить о «тройственном» результате функции: нормальный результат информация об ошибкe информация о том, что функция неприменима к данному набору параметров. Представим, что нам нужно имплементировать функцию, которая по текстовому значению, ожидая что это целое число, возвращает его знак «+» или «‑», а в случае 0 считается, что функция неприменима. Разумеется, входной параметр — это строка и возможно, не представляет никакое целое число.  Возможный подход в этом случае — первые две альтернативы паковать как и прежде в Result, но для обозначения тройственности сделать возвращаемое значение опциональным (возможно — null): Функции класса Result можно использовать в цепочках и каскадах вызовов.Ниже мы рассмотрим парочку самых типичных.Первый сценарий состоит в пребывании цепочки обработки или вычислений при первой встретившейся ошибке. При этом в случае ошибки мы хотим знать, на каком этапе она произошла. Представим, что мы хоти вычислить значение по формуле: При этом a, b, c заданы в виде строк и каждая строка может быть ошибочной. Вот возможное решение: Второй типичный сценарий, когда мы боремся с ошибками «до последнего». Так бывает, когда мы считываем «грязные» исходные данные, но только «чистые» можем отдать на дальнейшую обработку. В примере внизу (это последний пример!:‑) мы при конвертировании строки в целое число в случае неудачи с прямым конвертирование предположим, что строка представляет собой Double, а если и это не поможет, то выдадим максимальное целое значение: Если вы дочитали это руководство до этого места, я надеюсь — вам теперь понятно, что при обработке ошибок в Kotlin не стоит создавать собственные классы или приспосабливать для этого класс Either из библиотеки Arrow. Просто используйте правильно то, что уже в языке и так есть — класс Result. Напомню, что исходные коды для этой статьи вы найдёте здесь. Я надеюсь, эта статья помогла вам сформировать вам правильные ментальные модели обработки ошибок в Kotlin. Если тема ментальных моделей в программировании вас интересует, возможно вас заинтересуют эта и эта статьи. А если вы найдёте их интересными, присмотритесь или даже вступите в эту Телеграм‑группу.  А ещё я пишу книгу «Мемуары кочевого программиста: байки, были, думы». Если у вас есть время и настроение — я приглашаю вас полистать её страницы.  Системная Архитектура, Программирование