Обработка ошибок
Выбор между `throws`, `Result` и optional'ами; варианты `try`, `rethrows`, типизированные throws и проектирование типов ошибок, пригодных для UI.
8 вопросов
JuniorТеорияОчень частоКак брошенная ошибка поднимается по стеку вызовов через do/catch и что такое Error на самом деле?
Как брошенная ошибка поднимается по стеку вызовов через do/catch и что такое Error на самом деле?
throw разворачивает текущий вызов и доходит до ближайшего объемлющего catch; каждая функция на пути должна быть помечена throws или её поймать. Error — пустой протокол, которому может соответствовать любой тип, обычно enum, и соответствие делает значение бросаемым.
Типичные ошибки
- ✗Думают, что
Error— богатый базовый класс, а не пустой протокол - ✗Считают, что промежуточным функциям не нужен
throws, чтобы ошибка прошла - ✗Полагают, что бросать можно только классы, а не enum или struct
Уточняющие вопросы
- →Почему каждая функция на пути ошибки должна быть помечена
throws? - →Как типизированный
catch-шаблон выбирает один случай ошибки вместо другого?
JuniorТеорияОчень частоЧто делают try, try? и try! с выброшенной ошибкой и с типом возвращаемого значения?
Что делают try, try? и try! с выброшенной ошибкой и с типом возвращаемого значения?
try пробрасывает ошибку в объемлющий do/catch, сохраняя обычный тип возврата. try? глушит ошибку и делает результат optional, равным nil при сбое. try! утверждает, что ошибка не будет выброшена, и падает в рантайме, если она есть, отдавая обычное значение.
Типичные ошибки
- ✗Думают, что
try?возвращаетResult, а не optional - ✗Считают, что
try!при сбое отдаётnil, а не аварийно завершается - ✗Полагают, что обычный
tryсам перехватывает или глушит ошибку
Уточняющие вопросы
- →Когда
try?— плохой выбор, потому что вызывающему нужна причина? - →Какую гарантию рантайма вы даёте, когда пишете
try!?
MiddleТеорияЧастоКак ошибка покидает async throws-функцию и что происходит с Task, который бросает, когда его никто не ожидает?
Как ошибка покидает async throws-функцию и что происходит с Task, который бросает, когда его никто не ожидает?
async throws-функция пробрасывает ошибку как синхронная бросающая; вызывающий пишет try await, и ошибка всплывает на этом try. Task хранит ошибку в Result, доступную лишь через task.value. Если его никто не ожидает, ошибка молча отбрасывается.
Типичные ошибки
- ✗Считают, что незаожиданный бросающий
Taskпадает или логирует, а не отбрасывает ошибку - ✗Забывают
tryвtry awaitпри вызовеasync throws-функции - ✗Думают, что async-ошибки сами доходят до глобального обработчика без await
Уточняющие вопросы
- →Как
TaskGroupдоводит до сведения ошибку, брошенную дочерней задачей? - →Почему незаожиданный
Taskглотает ошибку, а не пробрасывает её?
MiddleКодЧастоНапишите failable init? и throwing init для одного парсящего типа — когда какой уместен?
Напишите failable init? и throwing init для одного парсящего типа — когда какой уместен?
Используйте init?, когда у сбоя одна неинтересная причина и nil говорит достаточно; вызывающий просто проверяет nil. Throwing init нужен, когда важна причина невалидности. init? отбрасывает причину, а throws несёт богатую пробрасываемую ошибку.
Типичные ошибки
- ✗Думают, что
init?может вернуть причину сбоя, как это делаетthrows - ✗Считают, что throwing
initвозвращаетnil, а не пробрасывает ошибку - ✗Полагают, что failable-инициализаторы бывают только у классов, а не у значимых типов
Уточняющие вопросы
- →Какой из двух лучше сочетается с
try?на месте вызова? - →Когда возврат
nilтеряет информацию, реально нужную вызывающему?
MiddleТеорияЧастоthrows против Result против optional-возврата — когда выбирать каждый и чего каждый стоит вызывающему?
throws против Result против optional-возврата — когда выбирать каждый и чего каждый стоит вызывающему?
Используйте throws для восстановимой ошибки, пробрасываемой через try; Result — чтобы нести успех-или-ошибку через callback- или хранимую границу; optional — когда причина сбоя неважна. Вызывающий платит try, switch или потерянной причиной.
Типичные ошибки
- ✗Считают
Resultиthrowsвзаимозаменяемыми, а не подходящими для синхронного против хранимого/async потока - ✗Возвращают optional там, где вызывающему нужна причина сбоя для отчёта
- ✗Думают, что optional-возврат несёт тип ошибки так же, как
throwsиResult
Уточняющие вопросы
- →Когда
Resultчитается лучшеthrowsдля API с completion-handler? - →Как превратить
throws-вызов вResult, не потеряв тип ошибки?
MiddleДизайнИногдаОшибка пересекает границу модуля. Ваш фреймворк вызывает нижележащую зависимость — сетевую библиотеку, парсер — которая бросает собственные типы ошибок, а потребитель вашего фреймворка ловит ошибки из вашего публичного API. Для каждого сбоя вы решаете: показать нижнюю ошибку потребителю как есть или обернуть её в свой тип и пробросить. Показ протаскивает зависимость в публичный API — смените парсер, и catch каждого потребителя ломается. Обёртывание прячет полезную диагностику, если не сохранить исходную причину. Спроектируйте границу ошибок модуля: какие ошибки оборачивать, а какие пропускать, как держать публичный тип ошибки стабильным при смене внутренностей, как сохранить исходную причину для отладки и как потребителю отличить восстановимый сбой от ошибки программиста через эту границу.
Ошибка пересекает границу модуля. Ваш фреймворк вызывает нижележащую зависимость — сетевую библиотеку, парсер — которая бросает собственные типы ошибок, а потребитель вашего фреймворка ловит ошибки из вашего публичного API. Для каждого сбоя вы решаете: показать нижнюю ошибку потребителю как есть или обернуть её в свой тип и пробросить. Показ протаскивает зависимость в публичный API — смените парсер, и catch каждого потребителя ломается. Обёртывание прячет полезную диагностику, если не сохранить исходную причину. Спроектируйте границу ошибок модуля: какие ошибки оборачивать, а какие пропускать, как держать публичный тип ошибки стабильным при смене внутренностей, как сохранить исходную причину для отладки и как потребителю отличить восстановимый сбой от ошибки программиста через эту границу.
Оборачивайте ошибки зависимостей в тип модуля, чтобы публичный API не протекал сторонней ошибкой; тогда catch потребителей переживёт замену внутренней библиотеки. Храните оригинал как нижележащую причину для логов. Пропускайте лишь ошибки, входящие в ваш контракт.
Типичные ошибки
- ✗Протаскивают конкретный тип ошибки зависимости через публичный API
- ✗Оборачивают ошибки, но теряют исходную причину, нужную для отладки
- ✗Смешивают восстановимые сбои и ошибки программиста в один случай
Уточняющие вопросы
- →Как сохранить исходную ошибку, показывая при этом собственный тип?
- →Что ломается у потребителей, если публичный enum ошибок не заморожен?
MiddleКодИногдаНапишите функцию высшего порядка, которая бросает только если бросает её замыкание
Напишите функцию высшего порядка, которая бросает только если бросает её замыкание
rethrows помечает функцию, которая бросает только когда бросает переданное ей замыкание. С незабрасывающим замыканием вызывающий обходится без try; с забрасывающим — вызов становится throwing. Обычный throws заставлял бы каждого вызывающего писать try.
Типичные ошибки
- ✗Думают, что
rethrowsперехватывает или глушит ошибку замыкания - ✗Считают, что
rethrows-функция требуетtryдаже с незабрасывающим замыканием - ✗Путают
rethrowsс повтором операции при сбое
Уточняющие вопросы
- →Почему
rethrows-функция не может бросить собственную ошибку? - →Как меняется требование
tryу вызывающего в зависимости от переданного замыкания?
SeniorДизайнИногдаВы проектируете модель ошибок для сетевого iOS-приложения. В нём есть транспортный слой на сетевом API URLSession плюс декодирование, доменный слой с бизнес-правилами вроде нехватки средств и слой презентации на декларативном UI-фреймворке SwiftUI. Запросы падают по многим причинам — нет связи, 500, битый payload, истёкший токен, отклонённая операция. Продукт хочет, чтобы каждый сбой доходил до пользователя понятным локализованным сообщением с опциональным повтором, а инженеры — полной диагностики в логах и крэш-репортах. Спроектируйте модель ошибок: как отделить доменные ошибки от транспортных, как ошибка нижнего слоя оборачивается или мапится, пересекая каждую границу, как слой презентации превращает любую ошибку в сообщение пользователю и что вы логируете против того, что показываете. Скажите, где применять typed throws, а где нет, и как держать модель стабильной при появлении новых видов сбоев.
Вы проектируете модель ошибок для сетевого iOS-приложения. В нём есть транспортный слой на сетевом API URLSession плюс декодирование, доменный слой с бизнес-правилами вроде нехватки средств и слой презентации на декларативном UI-фреймворке SwiftUI. Запросы падают по многим причинам — нет связи, 500, битый payload, истёкший токен, отклонённая операция. Продукт хочет, чтобы каждый сбой доходил до пользователя понятным локализованным сообщением с опциональным повтором, а инженеры — полной диагностики в логах и крэш-репортах. Спроектируйте модель ошибок: как отделить доменные ошибки от транспортных, как ошибка нижнего слоя оборачивается или мапится, пересекая каждую границу, как слой презентации превращает любую ошибку в сообщение пользователю и что вы логируете против того, что показываете. Скажите, где применять typed throws, а где нет, и как держать модель стабильной при появлении новых видов сбоев.
Отделяйте транспортные ошибки от доменных. Каждая граница оборачивает нижнюю ошибку в свой тип, сохраняя причину. Презентация мапит ошибку в локализованное сообщение и флаг повтора; логи хранят всю цепочку, скрытую от пользователя. Typed throws — только где набор сбоёв закрыт.
Типичные ошибки
- ✗Сваливают транспортные и доменные ошибки в один плоский enum на всё приложение
- ✗Показывают пользователю сырую нижнюю ошибку вместо смапленного локализованного сообщения
- ✗Применяют typed throws везде, даже где набор сбоёв слоя открыт и заранее неизвестен
Уточняющие вопросы
- →Где обёртывание ошибки теряет контекст и как сохранить исходную причину?
- →Почему typed throws вредит публичному API, чей набор сбоёв ещё может расти?