Представьте API, где ошибка возвращается как Result: обязан ли вызывающий код обработать failure так же стр...

Представьте API, где ошибка возвращается как Result: обязан ли вызывающий код обработать failure так же строго, как ошибку из throws?

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

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

Нет. Result — обычное значение, поэтому компилятор не требует разобрать его .failure: результат можно сохранить, передать дальше или даже проигнорировать. При вызове функции с throws компилятор требует явно обозначить возможность ошибки через try и затем либо обработать её, либо передать выше.

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

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

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

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

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

Предположим, сетевой слой возвращает Result<Data, NetworkError>. Получатель может проверить .success, обработать .failure, передать результат в другой слой или случайно проигнорировать его. Компилятор не знает, какое из этих решений было логически правильным для конкретного сценария.

С throws вызов нельзя написать как обычное выражение: нужно использовать try, обработать ошибку через do-catch, передать её вызывающему коду или явно выбрать подавление (try?) либо принудительное ожидание успеха (try!). Поэтому throws лучше защищает от случайного забытого пути обработки, но не заменяет проектирование политики ошибок.

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

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

func read() throws -> Int { throw NSError(domain: "Read", code: 1) } func load() throws -> Int { try read() } let value = try? read()

В load ошибка не обработана локально, а неявно передана выше, поскольку сама функция тоже объявлена как throws. В последней строке ошибка преобразована в nil; компилятор принимает такой код, потому что try? — явный выбор стратегии подавления.

У Result<Success, Failure> ошибка находится внутри обычного значения. Тип сообщает, какие варианты возможны, но не заставляет потребителя извлечь один из них. Даже если Failure — конкретный доменный тип, компилятор не требует вызвать switch, mapError, get() или другой метод обработки.

Компромисс зависит от границы API. throws обычно удобнее для последовательной операции, где ошибка должна прервать текущий сценарий и подняться по стеку. Result удобнее, когда результат нужно сохранить, передать как значение, объединить с другими данными или обработать в другом месте жизненного цикла.

Типизированный Result также делает тип ошибки видимым в сигнатуре. Но видимость типа не равна обязательной обработке: это преимущество моделирования данных, а не статической проверки того, что .failure действительно рассмотрен.

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

Слой загрузки документов запускает запрос, а экран может исчезнуть до завершения операции. Команда рассматривает два варианта: использовать throws внутри async-цепочки или передавать Result в callback.

throws делает последовательный сценарий компактным и заставляет каждый промежуточный вызывающий уровень явно выбрать обработку или проброс. Однако для callback-API результат естественно передаётся как одно значение, а его обработку можно централизовать в координаторе.

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

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

  1. Вопрос: означает ли обязательный try, что ошибка обязательно будет обработана?

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

  2. Вопрос: можно ли считать Result безопаснее только потому, что его ошибка указана в типе?

    Нет. Конкретный тип Failure помогает программно различать причины ошибки и предотвращает передачу несовместимых типов, но значение всё ещё можно проигнорировать. Безопасность Result достигается соглашениями API, тестами и явной обработкой вариантов, а не самим фактом использования этого типа.

  3. Вопрос: когда переход с throws на Result ухудшает API?

    Он ухудшает API, если результат операции почти всегда должен немедленно прервать текущую последовательность и подняться к ближайшему уровню, где есть контекст для обработки. В таком случае Result создаёт дополнительное распаковывание на каждом шаге и позволяет случайно проигнорировать .failure; throws лучше выражает поток управления. Переход оправдан, когда результат действительно должен жить как данные: сохраняться, передаваться callback-ом или комбинироваться с другими значениями.