Практическая ситуация: бросающую функцию нужно представить как Result. Какой результат создаст этот код и за счёт какого механизма ошибка попадёт в failure?
enum LoadError: Error {
case offline
}
func load() throws -> Int {
throw LoadError.offline
}
let result = Result { try load() }
Код создаст Result<Int, Error>.failure(LoadError.offline). Инициализатор Result(catching:) немедленно выполняет переданную замыкающую функцию: возвращённое значение превращает в success, а выброшенную ошибку — в failure.
Тип ошибки не преобразуется в текст и не теряется: экземпляр LoadError.offline сохраняется внутри Result как значение типа Error.
Модель throws предназначена для передачи ошибки по стеку вызовов до ближайшего обработчика. Она удобна, когда ошибка является частью управления потоком и вызывающий код готов использовать do-catch.
Result решает другую задачу: представляет успех или ошибку обычным значением. Это удобно для API, где результат нужно сохранить, передать в замыкание, объединить с другими значениями или обработать позднее без немедленного перехвата исключения.
Инициализатор Result(catching:) связывает эти две модели и позволяет адаптировать существующую бросающую функцию к интерфейсу, возвращающему Result.
Допустим, библиотечная функция уже объявлена как throws, но слой приложения должен вернуть единое значение Result — например, для передачи результата в очередь, кэш или callback. Простое присваивание результата вызова невозможно: try load() либо возвращает Int, либо передаёт ошибку дальше.
Ручная адаптация через do-catch работает, но добавляет шаблонный код. При неаккуратной реализации можно случайно преобразовать ошибку в строку и потерять её тип, контекст и возможность pattern matching.
Выражение Result { try load() } использует Result(catching:). Замыкание выполняется сразу во время создания result, поэтому вызов load() не откладывается до последующего чтения значения.
Механизм эквивалентен следующей логике:
Если замыкание завершится обычным return, будет создан .success(value). Если внутри произойдёт throw, будет создан .failure(error). В данном примере ошибка имеет динамический тип LoadError, хотя тип свойства failure — Error.
Преимущество такого подхода — сохранение исходной ошибки. Вызывающий код может проверить её тип:
Ограничение заключается в том, что Result(catching:) предназначен для ошибок, выбрасываемых из замыкания. Он не превращает аварийные состояния вроде fatalError в обычный failure.
Если вызывающий код всё равно сразу делает switch или вызывает try result.get(), использование Result может быть избыточным: в таком месте throws обычно проще. Result оправдан, когда состояние успеха или ошибки нужно хранить, передавать или комбинировать как данные.
Сервис загрузки данных уже предоставляет func fetch() throws -> Model, но слой аналитики должен передать итог операции в callback, который принимает Result<Model, Error>.
Рассматривались три варианта:
fetch на возврат Result — это ломает существующих вызывающих и распространяет выбранную модель на весь API;do-catch — решение прозрачно, но содержит повторяющийся код адаптации;Result { try fetch() } — сохраняет исходную ошибку и не меняет API сервиса.Выбран третий вариант. Результат создаётся в момент выполнения операции, а callback получает либо модель, либо исходный объект ошибки и может отдельно обработать сетевую, серверную или доменную причину сбоя. Это уменьшает связность между сервисом и слоем доставки результата.
Вопрос: Какой тип ошибки будет доступен в result.failure, если load() выбрасывает конкретный тип LoadError?
Ответ: Статический тип значения обычно будет Error, потому что Result(catching:) создаёт результат с типом ошибки Error. Однако сам объект внутри сохраняет динамический тип LoadError, поэтому его можно проверить приведением или pattern matching. Преобразование в Error не означает преобразование в строку и не уничтожает конкретное значение ошибки.
Вопрос: Что произойдёт, если вызвать result.get() для результата .failure?
Ответ: get() вернёт значение для .success, а для .failure выбросит сохранённую ошибку. Это позволяет преобразовать значение Result обратно в модель throws, но такой вызов требует try и подходящего обработчика. Если ошибка уже должна быть обработана через switch, вызов get() добавляет лишний переход между двумя моделями.
Вопрос: Почему Result(catching:) не делает асинхронную операцию асинхронной автоматически?
Ответ: Инициализатор принимает обычное синхронное бросающее замыкание и выполняет его в текущем контексте. Если операция имеет вид async throws, её нужно вызывать из async-контекста и отдельно выбрать модель представления результата — например, оставить async throws или написать собственный асинхронный адаптер. Сам Result хранит итоговое значение, но не управляет ожиданием, отменой или планированием задачи.