Как передать ошибку вызывающему коду, не обрабатывая её на текущем уровне?

Как передать ошибку вызывающему коду, не обрабатывая её на текущем уровне?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Если текущая функция сама объявлена как throws, вызов другой выбрасывающей функции нужно выполнить с try, но не заключать в локальный do-catch. Возникшая ошибка автоматически покинет текущую функцию и передастся её вызывающему коду.

Исторический контекст

В Swift обработка ошибок отделена от обычного возвращаемого значения. Такой подход позволяет явно обозначить в сигнатуре функции возможность сбоя и не заставляет каждый промежуточный уровень немедленно принимать решение о том, как его обработать.

Модель особенно полезна для многоуровневых приложений: нижний слой сообщает об ошибке, промежуточные слои могут её безопасно передать, а граница приложения преобразует её в пользовательское сообщение или другой результат.

Постановка проблемы

Предположим, репозиторий загружает данные, сервис применяет бизнес-правила, а экран отображает результат. Если сервис перехватит ошибку только потому, что обязан вызвать throws-функцию, он может потерять исходную причину или преждевременно связать инфраструктурную ошибку с интерфейсом.

Неверное использование try? превращает любую ошибку в nil, а try! приводит к аварийному завершению при сбое. Поэтому промежуточный уровень должен уметь не обрабатывать ошибку, а корректно делегировать её выше.

Подробное решение

Вызов с try только разрешает операции передать ошибку наружу; сам по себе try её не перехватывает. Если вызывающая функция имеет throws, ошибка немедленно завершает текущую функцию и становится результатом её неуспешного завершения.

enum LoadError: Error { case unavailable } func loadValue() throws -> String { throw LoadError.unavailable } func prepareValue() throws -> String { let value = try loadValue() return value } do { print(try prepareValue()) } catch { print("Ошибка: \(error)") }

В примере prepareValue не знает, как исправить сбой, поэтому объявлена как throws. Ошибка проходит через неё и обрабатывается в do-catch на внешнем уровне.

Это отличается от try?, который преобразует ошибку в отсутствие значения, и от try!, который предполагает отсутствие ошибки и аварийно завершает выполнение при нарушении этого предположения. Если текущий уровень действительно владеет контекстом обработки, он может использовать do-catch, при необходимости преобразовав ошибку в доменный тип.

Обязательное условие — вызывающая функция также должна иметь способ завершиться без обычного значения: объявить throws, обработать ошибку через do-catch либо явно выбрать try? или try!. Простое добавление try не делает функцию безопасной и не устраняет необходимость определить границу обработки.

Ситуация из практики

Сервис оформления заказа вызывает репозиторий платежей. Репозиторий может сообщить, что сеть недоступна, платёж отклонён или запрос истёк по времени.

Рассматривались три варианта:

  • Перехватывать все ошибки в репозитории — просто для вызывающего кода, но бизнес-слой теряет возможность различать инфраструктурные причины.
  • Использовать try? в сервисе — не приводит к аварийному завершению, но превращает разные причины в одинаковое отсутствие результата.
  • Передать ошибку через throws — сохраняет тип и контекст, но требует явно определить точку окончательной обработки.

Выбран третий вариант: репозиторий и сервис пробрасывают ошибку, а граница приложения преобразует известные причины в состояния интерфейса. В результате технические детали не смешиваются с UI, а неожиданные ошибки не маскируются под обычное отсутствие данных.

Что кандидаты часто упускают

  1. Вопрос: Можно ли пробросить ошибку из функции, которая сама не объявлена как throws?

    Ответ: Нет, если речь идёт о нормальном вызове выбрасывающей функции. Такая функция должна либо обработать ошибку внутри do-catch, либо изменить свою сигнатуру и объявить throws. Исключения составляют явные преобразования через try? и try!, но они меняют семантику: первое скрывает ошибку за nil, второе переносит ответственность за отсутствие ошибки на разработчика.

  2. Вопрос: Сохраняет ли проброшенная ошибка свой исходный тип?

    Ответ: При обычном пробросе ошибка передаётся как значение, соответствующее протоколу Error, без обязательного преобразования в новый тип. Поэтому внешний catch может сопоставить её с конкретным типом или использовать общий обработчик. Если промежуточный слой сознательно оборачивает ошибку в собственный доменный тип, исходную причину следует сохранить отдельно, если она нужна для диагностики или последующего анализа.

  3. Вопрос: Что происходит с выполнением текущей функции после того, как try обнаружил ошибку?

    Ответ: Обычное выполнение функции прекращается в точке неуспешного вызова, а управление передаётся ближайшему подходящему обработчику выше по стеку. Инструкции после этого вызова в текущей функции не выполняются. Зарегистрированные ранее блоки defer при выходе из функции выполняются перед фактическим возвратом ошибки, поэтому их используют для освобождения ресурсов и восстановления инвариантов, но не как замену обработке ошибки.