Обработка ошибок
Как Kotlin сообщает об ошибках — тип Result и runCatching, сознательный отказ от checked exceptions и выбор между возвратом ожидаемой ошибки и выбросом исключения.
6 вопросов
JuniorТеорияОчень частоЕсть ли в Kotlin checked exceptions и зачем тогда нужна @Throws?
Есть ли в Kotlin checked exceptions и зачем тогда нужна @Throws?
Нет. Любое исключение в Kotlin — unchecked: ничто не заставляет вызывающий код ловить его или перечислять в сигнатуре, и компилятор этого не проверяет. @Throws нужна исключительно для интеропа с Java — она порождает throws-клаузу, которую видит Java-код.
Типичные ошибки
- ✗Считать, что Kotlin наследует checked exceptions из Java для всего, что наследует
Exception - ✗Думать, что
@Throwsзаставляет Kotlin-вызывающих ловить перечисленные исключения - ✗Полагать, что неаннотированная Kotlin-функция гарантированно ничего не бросает
Уточняющие вопросы
- →Почему Java-код не может написать
catch (IOException e)вокруг неаннотированного вызова Kotlin? - →Меняет ли
@Throwsхоть что-то для Kotlin-вызывающего той же функции?
JuniorТеорияЧастоЧто такое Result<T> в Kotlin и как достать из него значение?
Что такое Result<T> в Kotlin и как достать из него значение?
Result<T> — одно значение, которое хранит либо успешный результат, либо Throwable, на котором вызов сорвался. Читают его через isSuccess, getOrNull() или fold(). Ошибка становится обычным возвращаемым значением, а не прыжком из вызова.
Типичные ошибки
- ✗Думать, что
Result<T>теряетThrowableи лишь сигналит об успехе или ошибке - ✗Считать, что чтение неуспешного
Resultперебрасывает, а не отдаёт ошибку - ✗Полагать, что для возврата
Result<T>нужен флаг компилятора
Уточняющие вопросы
- →Что даёт
fold(), чего не даёт обычная проверкаgetOrNull()? - →Чем
Result<T>отличается отsealed-типа ошибок, объявленного вами самими?
MiddleТеорияЧастоВ чём компромисс полного отказа Kotlin от checked exceptions?
В чём компромисс полного отказа Kotlin от checked exceptions?
Приобретаете: никакого throws-бойлерплейта и церемонии «поймал и проглотил». Теряете подсказку компилятора, что вызов может сорваться — сигнатура молчит. Восстановимые сбои моделируются вручную через Result или sealed-тип ошибок, а что бросается — документирует автор.
Типичные ошибки
- ✗Думать, что Kotlin всё ещё проверяет всё, что наследует
Exception, как Java - ✗Ждать от компилятора предупреждения на месте вызова, что функция может бросить
- ✗Считать, что
runCatchingвокруг каждого вызова возвращает утраченную гарантию компилятора
Уточняющие вопросы
- →Когда
sealed-тип ошибок здесь лучше, чем возвратResult<T>? - →Что должен говорить KDoc-тег
@throwsу функции без@Throws?
MiddleДебаггингЧастоПочему эта корутина продолжает работать после отмены её scope?
Почему эта корутина продолжает работать после отмены её scope?
runCatching ловит Throwable, поэтому глотает и CancellationException, которым отмена и приходит: корутина принимает отмену за обычный сбой, логирует его и идёт дальше. Перебросьте её или ловите только те исключения, на которые способны реагировать.
Типичные ошибки
- ✗Думать, что
runCatchingловит лишьExceptionи не трогаетCancellationException - ✗Принимать пойманный
CancellationExceptionза настоящий сбой, достойный лога и ретрая - ✗Считать, что
getOrNull()на неуспешномResultперебрасывает и тем останавливает корутину
Уточняющие вопросы
- →Почему тот же баг возникает и с обычным
try/catch (e: Throwable)? - →Что здесь меняет вызов
coroutineContext.ensureActive()внутриonFailure?
MiddleДизайнИногдаВы проектируете API платёжного модуля для Android-приложения. Вызов charge() может сорваться тремя способами: данные карты не проходят валидацию, сетевой вызов уходит в таймаут и — отдельно — вызывающий код передаёт отрицательную сумму, что является багом в этом коде. Требование команды: сбой, на который обязан отреагировать UI, вызывающий не должен иметь возможности забыть, а настоящая программная ошибка должна громко всплыть в крэш-репортинге, а не быть тихо обработанной. Модуль вызывается только из Kotlin. Решите для каждого из трёх сбоев, должен он быть значением, возвращаемым из charge(), или брошенным исключением; скажите, какой тип должен нести возвращаемый сбой; и обоснуйте это разделение.
Вы проектируете API платёжного модуля для Android-приложения. Вызов charge() может сорваться тремя способами: данные карты не проходят валидацию, сетевой вызов уходит в таймаут и — отдельно — вызывающий код передаёт отрицательную сумму, что является багом в этом коде. Требование команды: сбой, на который обязан отреагировать UI, вызывающий не должен иметь возможности забыть, а настоящая программная ошибка должна громко всплыть в крэш-репортинге, а не быть тихо обработанной. Модуль вызывается только из Kotlin. Решите для каждого из трёх сбоев, должен он быть значением, возвращаемым из charge(), или брошенным исключением; скажите, какой тип должен нести возвращаемый сбой; и обоснуйте это разделение.
Валидация и таймаут — ожидаемые восстановимые сбои: возвращайте их значением, которое вызывающий не сможет проигнорировать. sealed-тип ошибок лучше Result<T> — он именует каждый случай, и when по нему исчерпывающий. Отрицательная сумма — баг: бросайте, пусть всплывёт в крэш-репортинге.
Типичные ошибки
- ✗Моделировать программную ошибку как восстановимый сбой-значение вместо броска
- ✗Тянуться к
Result<T>там, где лучше подходитsealed-тип, именующий каждый случай сбоя - ✗Ждать, что
@Throwsзаставит Kotlin-вызывающего обработать брошенный сбой
Уточняющие вопросы
- →Как изменилось бы разделение, если бы модуль вызывался ещё и из Java?
- →Чем исчерпывающий
whenпоsealed-типу ошибок сильнее проверкиisFailure?
SeniorДизайнРедкоВы владеете общей Kotlin-библиотекой — обёрткой над HTTP-клиентом, — которая публикуется и для Kotlin-, и для Java-потребителей. Её fetch() может сорваться на некорректном URL, на ответе 4xx/5xx и на таймауте ввода-вывода, и потребители обязаны различать эти случаи. Коллега предлагает возвращать Result<Response> из каждой публичной функции и оборачивать тело каждой реализации в runCatching, чтобы за границу библиотеки ничего не улетало. Публичный API должен остаться удобным и честным из Java, без пристроенного сверху Kotlin-only слоя-хелпера. Спроектируйте публичную поверхность ошибок библиотеки: что должен отдавать fetch(), что реально видит Java-вызывающий на этапе компиляции и что ломает каждая половина предложения коллеги.
Вы владеете общей Kotlin-библиотекой — обёрткой над HTTP-клиентом, — которая публикуется и для Kotlin-, и для Java-потребителей. Её fetch() может сорваться на некорректном URL, на ответе 4xx/5xx и на таймауте ввода-вывода, и потребители обязаны различать эти случаи. Коллега предлагает возвращать Result<Response> из каждой публичной функции и оборачивать тело каждой реализации в runCatching, чтобы за границу библиотеки ничего не улетало. Публичный API должен остаться удобным и честным из Java, без пристроенного сверху Kotlin-only слоя-хелпера. Спроектируйте публичную поверхность ошибок библиотеки: что должен отдавать fetch(), что реально видит Java-вызывающий на этапе компиляции и что ломает каждая половина предложения коллеги.
Возвращайте из fetch() sealed-иерархию именованных доменных ошибок: компилятор принудит к исчерпывающему when. Result<T> — inline value class, поэтому Java видит искажённую боксированную сигнатуру; сигнал компиляции Java получает лишь там, где вы бросаете с @Throws. Не оборачивайте вызов целиком в runCatching.
Типичные ошибки
- ✗Выставлять
Result<T>через границу с Java и ждать чистой читаемой сигнатуры - ✗Оборачивать каждый публичный вызов в
runCatching, глотая и отмену, и настоящие баги - ✗Считать, что Java-вызывающие получат сигнал на этапе компиляции без броска с
@Throws
Уточняющие вопросы
- →Как сохранить такую
sealed-иерархию ошибок совместимой по исходникам при добавлении случаев? - →Что на деле порождает
@Throws(IOException::class)для Java-вызывающего этой функции?