Программирование SwiftОбработка ошибокРазработчик библиотек на Swift

Очистка ресурса в defer сама может выбросить ошибку, пока функция уже распространяет исходную ошибку. Какой...

Очистка ресурса в defer сама может выбросить ошибку, пока функция уже распространяет исходную ошибку. Какой способ обработки сохранит исходную причину?

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

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

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

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

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

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

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

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

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

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

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

enum OperationError: Error { case failed } enum CleanupError: Error { case closeFailed } func perform() throws { defer { do { try closeResource() } catch { print("Ошибка очистки: \(error)") } } try doWork() } func doWork() throws { throw OperationError.failed } func closeResource() throws { throw CleanupError.closeFailed }

В примере вызывающий код получает OperationError.failed, а CleanupError.closeFailed не теряется полностью: он фиксируется отдельно. Это обычно безопаснее, чем подменять исходную причину ошибкой очистки.

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

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

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

Сервис записывает данные во временный файл. Запись может завершиться ошибкой, а закрытие дескриптора — отдельной системной ошибкой.

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

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

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

  1. Можно ли просто написать throw внутри defer, чтобы передать ошибку очистки наружу?

    Нет, потенциально выбрасывающий вызов внутри defer должен быть обработан в самом теле defer; он не становится автоматически частью распространения ошибки функции. Кроме того, defer не предназначен для изменения управления через return, break или аналогичный выход из области.

  2. Что произойдёт с исходной ошибкой, если ошибка очистки будет принудительно обработана через try!?

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

  3. Как сообщить вызывающему коду об обеих ошибках, не скрывая ни одну?

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