Программирование SwiftОбработка ошибокРазработчик Swift, работающий с асинхронными сервисами

Допустимо ли выполнять await внутри defer в async функции?

Допустимо ли выполнять await внутри defer в async-функции?

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

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

Нет. Тело defer синхронное, поэтому оно не может содержать операцию await и приостановить выполнение. Для асинхронной очистки нужно явно вызвать её перед каждым выходом из области либо инкапсулировать такой жизненный цикл в отдельную асинхронную абстракцию.

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

defer решает задачу гарантированной синхронной очистки при выходе из области видимости: через обычный return, ошибку или другой переход управления. Его модель близка к конструкции finally, но не предусматривает приостановку выполнения.

С появлением async/await очистка ресурса иногда сама стала асинхронной. Однако defer не был превращён в асинхронный блок: выход из области должен оставаться предсказуемым и не создавать дополнительную точку приостановки.

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

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

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

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

defer выполняется только при выходе из своей области видимости, а не при каждой приостановке в await. Его тело не может быть async, поэтому асинхронную очистку необходимо вызвать явно в успешной и ошибочной ветках.

enum OperationError: Error { case failed } struct Resource { func close() async { print("closed") } } func use() async throws -> Int { let resource = Resource() do { let value = try await load() await resource.close() return value } catch { await resource.close() throw error } } func load() async throws -> Int { throw OperationError.failed }

Такой код гарантирует, что close() будет завершён до выхода из use(), а исходная ошибка будет повторно выброшена. Недостаток — дублирование очистки; его обычно устраняют асинхронной функцией-обёрткой, которая принимает операцию и централизованно управляет ресурсом.

Запуск Task { await resource.close() } внутри defer допустим как отдельное планирование работы, но не гарантирует её завершение до возврата из функции и не позволяет естественно объединить ошибку очистки с исходной ошибкой. Поэтому такой вариант подходит только когда очистка действительно может выполняться независимо.

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

Сервис открывает асинхронную транзакцию, выполняет несколько операций и обязан подтвердить её либо откатить до завершения метода. Использование defer удобно для синхронного освобождения памяти, но не подходит для асинхронных commit и rollback.

Рассматривались три варианта. Запускать закрытие через отдельную Task просто, но он не гарантирует порядок и завершение. Дублировать await commit и await rollback надёжно, однако увеличивает риск расхождения веток. Выбранной стала отдельная асинхронная обёртка управления транзакцией: она централизует успешное завершение, откат и правила обработки ошибок.

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

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

  1. Выполнится ли defer, если async-функция была отменена?

    Да, если отмена привела к обычному выходу из области видимости. Сама отмена в Swift обычно является состоянием задачи и не прерывает произвольный код автоматически; функция должна проверить отмену или вызвать отменяемую операцию, которая сообщит о ней. После такого выхода зарегистрированный синхронный defer выполнится.

  2. Можно ли вызвать обычный синхронный метод ресурса из defer внутри async-функции?

    Да. Ограничение касается именно приостанавливающих операций. Синхронная очистка в defer допустима и сохраняет обычную гарантию выполнения при выходе из области.

  3. Что произойдёт, если асинхронная очистка сама выбросит ошибку?

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