Команда Spring АйО не могла остаться в стороне и не прокомментировать одну из самых обсуждаемых новинок Kotlin, анонсированную на KotlinConf 2025 — Rich Errors. Вместо того чтобы выбрасывать исключения, теперь функции могут возвращать возможные ошибки как часть своей сигнатуры:
fun fetchUser(): User | NetworkErrorТакой подход делает потенциальные сбои явными, упрощает тестирование и избавляет от try-catch для предсказуемых ошибок. Новинка уже доступна в Kotlin 2.4 и, по мнению авторов, особенно полезна в бизнес-логике.
Но в сообществе мнения разделились.
Что говорят сторонники Rich Errors?
- Это логичное продолжение идеи безопасности типов, как - null safety.
- Ошибки становятся частью API — теперь явно видно, какие именно проблемы могут возникнуть. 
- try-catchбольше не нужен там, где ошибка — ожидаемый результат.
- Тестировать становится проще: вместо моков и исключений — обычная проверка значения. 
- Хорошо сочетается с функциональными паттернами без необходимости подключать сторонние библиотеки. 
Что вызывает сомнения?
- В контроллерах и сервисах с большим числом потенциальных ошибок сигнатуры методов становятся громоздкими. 
- Нет способа элегантно агрегировать ошибки: - A | B | Cработает, но не имеет общей семантики.
- В рамках Spring-приложений реалистичная польза ограничена — фреймворки останутся на исключениях. 
- Добавление такого типа обработки может серьёзно сказаться на времени компиляции. 
И что теперь?
Для одних Rich Errors — это долгожданное улучшение и эволюция Kotlin. Для других — спорный эксперимент, который добавляет сложности без ощутимой пользы в реальных проектах.
А вы как думаете? Используете ли Rich Errors в своём коде — или пока просто наблюдаете со стороны?

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано
Комментарии (27)
 - ETOZHEKIM25.07.2025 14:24- Удобно видеть, какие ошибки могут возникнуть при вызове метода. Не удобно, что они возвращаются функцией, обработка будет выглядеть странно. В try catch ты получаешь явно объект ошибки, думаю поэтому его и придумали. Как будто это шаг назад  - KivApple25.07.2025 14:24- Должна быть функциональная обвязка, как map/map_err в Rust. Тогда можно будет строить цепочки обработчиков.  - Anarchist25.07.2025 14:24- И в Rust, и в Scala (откуда, скорее всего и пришел такой синтаксис вариантов возвращаемых значений) есть даже более красивая конструкция - pattern matching. Я имею в виду функциональный, а не тот, который из регекспов. :) Плюс декомпозиция. Всё это в Котлине вроде уже есть. 
 
  - gBear25.07.2025 14:24- Не удобно, что они возвращаются функцией, ... - ?! Как раз - наоборот. В кои-то веки, "обшибки" - по честному - часть сигнатуры ф-ции. С нормальной композицией, выводом и т.п. - Если не понятно - User | NetworkError из примера - это полноценный тип. Эдакий AA union type, с единственным ограничением: AA это исключительно для подтипов Error. - Оно - может быть - и не универсально. Зато - сильно утилитарно получается. Как в Haskell - может быть и хочется, но не получится по очевидным причинам. А как в Scala - ну мы видели, к чему "оно" приводит :-) - ... обработка будет выглядеть странно - Да ладно?! Даже самый очевидный (но не единственный) вариант обработки - через when выглядит очень органично. - В try catch ты получаешь явно объект ошибки, думаю поэтому его и придумали. - Тут у нас отдельный высший тип для ошибок - Error. Т.е. они ("обшибки") - by design - не пересекаются с подтипами Any? - куда уж явней "объект ошибки" получать? - Единственное преимущество Throwable - это наличие stacktrace. Но и тут - если оно нам таки надо - всегда можно "раскрутиться" через !! оператор. - Как будто это шаг назад - В каком месте?!  - Dhwtj25.07.2025 14:24- А как в Scala - ну мы видели, к чему "оно" приводит :-) - Я не видел. И к чему же?  - gBear25.07.2025 14:24- Если коротко - каноничный AA-union-type штука сильно уж - скажем так - "обобщающая". Во всех смыслах этого слова. - И очень уж много нужно контроля (на уровне "усилий мозга"), чтобы пользоваться этим "правильно". А чуть "отвлекся" - и всё... "получилась какая-то нечитаемая хрень" :-( - И ещё больше усилий нужно для возврата в "контекст". Читать "это", порой, совсем "грустненько"... хотя и сам "это вчера только написал" :-)  - Anarchist25.07.2025 14:24- А можно пример "нечитаемой хрени"?  - Dhwtj25.07.2025 14:24- Видимо, такое - // Используем стандартный тип Either[A, B] для представления "или-или". // Договоримся, что слева (Left) - ошибка, справа (Right) - успех. // А как представить "Загрузку"?... Уже проблема. // Ну, допустим, обернем всё в Option. type WebRequestResult[T] = Option[Either[String, T]] // None => Загрузка // Some(Left("...")) => Ошибка // Some(Right(data)) => Успех- Использование - def printResult(result: WebRequestResult[Int]): Unit = { result match { case None => println("Загрузка...") case Some(either) => either match { case Left(msg) => println(s"Ошибка: $msg") case Right(data) => println(s"Успех! Данные: $data") } } }- один разработчик использует "каноничный" sealed trait, другой — Either, а третий придумывает свою структуру на tuple'ах. Все три подхода работают, но вместе превращают кодовую базу в ту самую "нечитаемую хрень  - Anarchist25.07.2025 14:24- Ну, здесь Option - вообще лишнее. Есть Future/Promise в стандартной библиотеке, Deferred в Cats, что-то похожее в ZIO. Это все композируется в более эффективные конструкции, чем регулярный опрос состояния. Посмотрите на такой вариант: - future andThen { case Left(msg) => println(s"Ошибка: $msg") case Right(data) => println(s"Успех! Данные: $data") }- Возможно, вам стоит пересмотреть архитектуру приложения? - Но если уж так хочется разделить три этих случая - алгебраические классы к вашим услугам. Если вы в третьей Scala, используйте enum. Во второй - три case-класса с общим предком. Типа такого: - trait Status[+T] { def print(): Unit } object Status { case object Running extends Status[Nothing] { def print(): Unit = println("Загрузка...") } final case class Success[T](data: T) extends Status[T] { def print(): Unit = (s"Успех! Данные: $data") } final case class Failure[T](msg: String) extends Status[Nothing] { def print(): Unit = println(s"Ошибка: $msg") } }
 
 
  - Dhwtj25.07.2025 14:24- Ну и кстати, в rust наиболее канонично видимо так - use std::path::PathBuf; // Ошибка #1: не удалось прочитать конфиг #[derive(Debug)] // для вывода pub struct ConfigError { pub path: PathBuf, pub reason: String, } // Ошибка #2: не удалось подключиться к БД #[derive(Debug)] pub struct DbError { pub host: String, pub port: u16, pub message: String, } // Функции, которые их возвращают (заглушки) fn load_config() -> Result<(), ConfigError> { /* ... */ Err(ConfigError { path: "cfg.toml".into(), reason: "permission denied".into() }) } fn connect_db() -> Result<(), DbError> { /* ... */ Ok(()) }- Использование - use anyhow::Result; fn run_anyhow() -> Result<()> { load_config()?; connect_db()?; Ok(()) } fn main() { if let Err(e) = run_anyhow() { // e - это `anyhow::Error`, который хранит исходную ошибку внутри println!("Ошибка: {}", e); // При желании, можно добраться до исходного типа if let Some(config_err) = e.downcast_ref::<ConfigError>() { println!("Проблема с конфигом: {}", config_err.path.display()); } } }
 
 
 
 
 - Dhwtj25.07.2025 14:24- Теперь почти как в rust)) - Интеграция с фреймворками: Самый веский практический аргумент. Этот подход лучше всего работает в ядре бизнес-логики, изолированном от фреймворка. На границе (в контроллерах) пишется адаптер: when (result) { is Ok -> ..., is Err -> mapToHttpError(...) }. Пытаться тянуть Result до самого Spring MVC — плохая идея  - gBear25.07.2025 14:24- Пытаться тянуть Result до самого Spring MVC — плохая идея - "Прикол" в том, что тут как раз нет Result'а. Ни в терминах rust'а, ни в терминах kotlin'а. - Result - это "эвфемизм" более общего Either. А Either - by design - скажем так, "хромает" в плане композиции "левой" своей части. Т.е. "левая" часть композиции выводится в что-то осмысленное только при прям очень сильных ограничениях, накладываемых на. Чего - по понятным причинам - делать вообще не хочется. - Result - соответственно - "хромает" на "правую" свою часть. - То, что предлагается ребятами из kotlin - позволяет с одной стороны - избежать такого рода "хромоты". А с другой - не скатится в AA-union-hell. - Насколько - в реальности - окажутся сильными ограничения, накладываемые на подтипы Error - это будем делать посмотреть (с). Дизайн пока не финализирован. Но то, что есть сейчас не выглядит сколь-нибудь "страшным". - Я к тому, что иметь - условный - - private val <T:Any> (T|Error).responseEntity: ResponseEntity<out Any> = get() -> when { ...}- на уровне контроллера (или даже его пакета) - не выглядит, имхо, чем-то прям "ужос-ужос". Error и upper-bound - понятно, "в реальности" заменяются на какие-то более осмысленные type aliase'ы. - А вот как раз "городьба" отдельной иерархии для передачи "типизации в ошибках" в контроллер - при условии наличия rich errors - будет выглядеть "немножко странно". Нет?  - Dhwtj25.07.2025 14:24- Да, в этом смысле удобнее. - Тип возврата становится объединением всех возможных ошибок, а не сложной иерархией. В раст нет анонимного типа объединения, его надо заранее создать. А в Котлин оно будет анонимным, создаваемым автоматически 
  - moonster25.07.2025 14:24- За последние несколько лет я запилил десятки контроллеров. По итогу пришел как раз к примерно такой схеме - сервис, который обслуживает контроллер, всегда возвращает Either. Может, конечно и исключение бросить, но это всегда 500. - Сначала были исключения на все случаи, но потом оказалось, что нет надежного способа заставить в юнит тестах контроллеров из моков сервиса кидать те же исключения, что кидаются из настоящих. 
 
 
 - gBear25.07.2025 14:24- Сугубое имхо... - Выглядит оно уже сейчас сильно "вкусно". Ребята из arrow - уже облизываются :-) Наконец-то нормальная "человечья" error composition "из коробки". - Озвученные "сомнения" - в разрезе Kotlin - выглядят, мягко говоря, неубедительно. - Яб таки дождался финализации. Возможно, оно действительно "негативненько" скажется на interop'е со стороны Java. Возможно (ну а вдруг), придумают как таки обойтись - в этой части - без "костыликов". Но это пока единственное, что хоть сколько-то "пугает". 
 - ilja90325.07.2025 14:24- Какой-то недо typescript. Тулбокс для всего этого маленький, зачем возвращать ошибку если можно было бы возвращать разные классы например. Типов нет, а семантика как будто есть. Котлин стал не лучшей джавой, а недо сишарпом. 
 - ris58h25.07.2025 14:24- Есть где-нибудь нормальное краткое описание фичи? 45-минутное видео смотреть не горю желанием. - Не понял как "кидается" ошибка. Через throw или через return? Могу ли я создать функцию, которая возвращает и кидает ошибку того же класса одновременно? Если да, то как понять был ли это успешный возврат или ошибка?  - AWE6425.07.2025 14:24- перебирай типы, в котлине не в моде полиморфизм. И не задавай лишних вопросов, а радуйся, радуйся! 
  - gBear25.07.2025 14:24- Есть где-нибудь нормальное краткое описание фичи? - Не понял как "кидается" ошибка. Через throw или через return? - throw никак не меняется. Соответственно, не имеет смысла для подтипов Error. Т.е. "ошибка" возвращается штатно - через return. - Могу ли я создать функцию, которая возвращает и кидает ошибку того же класса одновременно? - Нет. Но если очень хочется, через !! можно кинуть KotlinException, который "обернет" Error. - Если да, то как понять был ли это успешный возврат или ошибка? - Все "ошибки" - это подтипы Error. Error - это новый отдельный top type. Т.е. и он, и его подтипы не приводятся к Any?. Соответственно, на "или ошибка" можно проверять, банально, через is Error. 
 
 - Spyman25.07.2025 14:24- Ну т.е. аналог того, что мы влзващали бы ошибку или результат как варианты sealed класса, только теперь меньше кода надо писать, если ошибки одинаковые в разных методах. В целом неплохо, но и не то, чтобы прямо интересно. У exception уникальная механика пробрасывания ошибки вверх по цепочке вызовов и finaly гарантирующий выполнение, а тут по сути просто синтаскический сахар. 
 - qw125.07.2025 14:24- Получится так, что придётся ловить и старые исключения, и обращать внимание на возвращаемые ошибки. Ведь никто не будет переписывать всю библиотеку классов, чтобы функция чтения файла стала возвращать все возможные виды ошибок. - Особенно "приятно" будет писать высокоуровневый код. 
 Например, по бизнес-логике функция- getUser(int id)возвращает- Userили- UserNotFoundError, теперь в сигнатуру придётся тащить всё, что у неё под капотом: если юзер читается из БД, то и все database-ошибки и ошибки сети, если из файла - то все файловые ошибки (включая отсутствие прав). Если что-то меняется слоем ниже, придётся глобально переписывать все слои вверх. - qw125.07.2025 14:24- Ну кстати, если не специфицировать, какие конкретно исключения выбрасываются, а поместить в сигнатуру корень иерархии - java.lang.Exception, то абстракция (сигнатура функции) не будет зависеть от реализации. Если обработчику потребуется поклассифицировать ошибки, можно посмотреть на тип объекта Exception.- Что от этого выигрываем? Удобнее комбинировать операции в функциональном стиле (монады), меньше расходы на выбрасывание и ловлю исключений (но теряем StackTrace, а иногда это интересно для разбора ошибок). 
 
 
           
 
ExTalosDx
Spring могут и доработать.
Нет смысла спешить с мнением, кмк.
С моей точки зрения это более явный и более удобный optional. Только в разрезе не только null safety.
Так же могу накинуть минус: кто-то может сказать что это checked exception 2.0.
Потому что позволяет одновременно проверять насколько ошибок в более удобном "try-catch (when)".
Но для меня это плюс, хоть и тоже самое, но читать такой код как в примере на презентации крайне приятно.
Anarchist
Нет, это совсем не checked exceptions. Это попытка уйти от исключений в принципе. Кроме того, написать лямбда функцию с checked exception невозможно. А с такой возвращаемой ошибкой - пожалуйста. Исключения - одна из родовых травм Java, и всем JVM-языкам приходится возиться с ними, к сожалению.
rsashka
Родовая травма языка, это отсутствие исключений, когда они физически присутствуют в любой компьютерной программе, но разработчики изобретают разные способы их сокрытия, чтобы не заниматься из обработкой
Anarchist
Передача значений из оперативки в регистры процессора физически присутствуют в любой программе. Или "это другое"?
rsashka
Не понял, причем тут передача значений из оперативки в регистры