После регистрации выбрасывающего замыкания в отложенный вызов сможет ли внешний do-catch перехватить ошибку, выброшенную позже?
Нет. Внешний do-catch перехватывает только ошибку, возникшую во время выполнения вызываемого внутри него кода. Если выбрасывающее замыкание сохранено и выполнено позже, его ошибка должна быть обработана в месте фактического вызова либо передана дальше через соответствующий механизм.
Модель throws отделяет описание возможности ошибки от момента её возникновения. Это позволяет передавать выбрасывающие операции как замыкания, но одновременно требует учитывать границы их фактического выполнения.
Такое разделение особенно важно для отложенных, событийных и асинхронных API: регистрация обработчика и его последующий запуск являются разными этапами управления потоком.
do-catch не устанавливает глобальный перехватчик для всех ошибок, связанных с операцией. Он действует только на динамический участок выполнения между входом в do и выходом из него.
Поэтому регистрация замыкания может завершиться успешно, а ошибка появится позднее — например, после события, таймера или завершения другой операции. Попытка обработать её внешним catch создаёт ложное ощущение безопасности: ошибка либо должна быть поймана внутри API, либо станет необработанной.
Вызывающий код должен помещать в do-catch именно вызов сохранённого замыкания:
В первом блоке выполняется только register; тело замыкания ещё не запущено. Во втором блоке выполняется pending, поэтому именно этот do-catch видит выброшенную ошибку.
Если API само вызывает замыкание позже, оно не может передать ошибку в уже завершившийся внешний do-catch. Возможные контракты API — обработать ошибку внутри, вернуть её через Result, передать в callback или предоставить отдельный бросающий метод, который выполняет операцию синхронно.
Для async-кода действует тот же принцип: ошибка из задачи перехватывается в коде, который ожидает её через try await, либо внутри самой задачи. Сам факт создания задачи не связывает её ошибки с окружающим синхронным do-catch.
Слой планировщика принимает @escaping () throws -> Void и запускает действие после получения сетевого события. Разработчик регистрирует действие внутри do-catch и ожидает, что ошибка преобразования данных попадёт во внешний catch.
Вариант с внешним do-catch неверен: к моменту запуска callback блок уже завершён. Обработка внутри планировщика позволяет централизованно записать ошибку, но скрывает её от вызывающего кода. Передача ошибки через callback сохраняет информацию, однако усложняет контракт и требует согласовать жизненный цикл обработчика.
Наиболее подходящее решение зависит от API: для событийного слоя обычно используют callback или Result, а для операции, которую можно явно ожидать, — async throws и try await. В обоих случаях ошибка обрабатывается рядом с точкой фактического выполнения, поэтому она не теряется и не связывается с уже завершённым стеком вызовов.
1. Если замыкание не escaping и вызывается внутри do, сработает ли внешний catch?
Да, если вызов замыкания происходит непосредственно внутри этого do и перед ним стоит try. Для неэкранирующего замыкания вызов обычно завершается до выхода из функции, поэтому ошибка распространяется по обычному стеку вызовов и достигает окружающего catch.
Ключевым является не само наличие замыкания, а момент и место его выполнения. Атрибут @escaping лишь разрешает сохранить замыкание для более позднего вызова; он не делает ошибку автоматически асинхронной, но часто указывает на такую границу времени.
2. Может ли невыбрасывающий API передать наружу ошибку из переданного ему выбрасывающего callback?
Не через обычное распространение throws. Если API вызывает callback позже, его собственный метод уже не находится в стеке вызывающего do-catch. API должен сам обработать ошибку, передать её через callback или изменить контракт так, чтобы результат операции был доступен вызывающему коду.
Одной декларации параметра как throws недостаточно: она разрешает callback выбросить ошибку, но не определяет, кто обязан её перехватить после отложенного запуска.
3. Что изменится, если ошибка возникнет внутри отдельной Task?
Окружающий синхронный do-catch её не перехватит, потому что тело задачи выполняется в отдельном контексте. Ошибку нужно обработать внутри задачи либо получить через операцию ожидания, например через try await у API, которое возвращает результат задачи.
Если задача создаётся как fire-and-forget и её ошибка нигде не извлекается и не обрабатывается, вызывающий код не получает обычного синхронного пути распространения этой ошибки. Поэтому для значимых ошибок задача должна иметь наблюдаемый контракт результата.