Что гарантированно произойдёт с defer, если функция завершится через fatalError?
defer не выполнится при завершении через fatalError. Эта функция имеет возвращаемый тип Never и немедленно прекращает выполнение программы, поэтому обычный выход из области видимости, при котором срабатывает defer, не происходит.
defer предназначен для структурированного освобождения ресурсов при нормальном выходе из области видимости: через return, выброс ошибки или достижение закрывающей скобки. Такой подход уменьшает дублирование завершающих действий в разных ветках функции.
Однако defer не является механизмом гарантированного завершения процесса. Он работает только пока Swift продолжает управляемое выполнение программы.
Разработчик может ошибочно считать, что defer всегда выполнит очистку, включая аварийное завершение. Это опасно: при fatalError не выполнятся закрытие файла, разблокировка ресурса, запись журнала или восстановление состояния, размещённые в defer.
Минимальный пример:
Строка cleanup не будет напечатана: выполнение прекращается внутри fatalError.
fatalError не выбрасывает значение типа Error. Он немедленно останавливает выполнение и формально возвращает Never — тип, у которого нет возможного значения. Поэтому стек вызовов не разматывается как при обычном throw, а инструкции defer не запускаются.
Это отличается от обрабатываемой ошибки. Если функция выполняет throw, Swift выходит из текущей области видимости по механизму обработки ошибок и перед этим выполняет зарегистрированные defer. При return происходит аналогичный управляемый выход.
defer также не следует рассматривать как защиту от внешнего принудительного завершения процесса, например убийства приложения операционной системой или аварии среды выполнения. Для критически важного состояния нужны операции, выполняемые заранее или в транзакционном стиле, а не надежда на финальную очистку.
Использовать fatalError следует только для действительно невозможных состояний, нарушений внутренних инвариантов или ошибок разработки. Для ожидаемых отказов — отсутствия сети, неверных данных, запрета доступа — нужно моделировать ошибку через throws или Result, чтобы вызывающий код мог принять решение.
Сервис загружает конфигурацию и временно меняет глобальный режим приложения. При обычной ошибке загрузки режим нужно восстановить.
Вариант с defer корректен для ожидаемых ошибок: восстановление выполнится и при return, и при throw. Вариант с fatalError формально короче, но при повреждённой конфигурации оставит процесс без гарантированной очистки и, вероятно, завершит приложение без полезной диагностики.
Выбранное решение — вернуть типизированную ошибку через throws, а defer использовать только для локальной очистки. Если состояние настолько нарушено, что продолжение невозможно, приложение можно завершить после явной записи критической информации, не полагаясь на defer.
Результат: ожидаемые сбои становятся тестируемыми и обрабатываемыми, а аварийное завершение остаётся исключительным сценарием для невосстановимых нарушений инвариантов.
1. Выполнится ли defer, если перед fatalError вызвать функцию, которая возвращает Never?
Нет. Любой последующий код после вызова функции с результатом Never недостижим, а управляемого выхода из текущей области видимости не происходит. Сам факт наличия defer не меняет поведение Never.
2. Сработает ли defer, если ошибка была перехвачена во внешнем do-catch?
Да, если выход из области видимости происходит через обычный throw. Сначала выполнятся defer во время размотки текущей области, затем управление попадёт в подходящий внешний catch. Перехват ошибки не превращает throw в аварийное завершение.
3. Можно ли заменить fatalError на throw, если функция объявлена без throws?
Нет, обычный throw требует, чтобы функция или замыкание поддерживали передачу ошибки через throws. Если контракт менять нельзя, можно вернуть ошибку как значение, например через Result, либо явно завершить выполнение; но эти варианты имеют разные последствия для вызывающего кода. Для ожидаемого сбоя предпочтительнее изменить контракт на throws или Result, а не маскировать его через fatalError.