Программирование SwiftОбработка ошибокSwift-разработчик серверных и прикладных сервисов

Как адаптировать тип ошибки в Result к доменному типу, сохранив значение успешного результата?

Как адаптировать тип ошибки в Result к доменному типу, сохранив значение успешного результата?

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

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

Используйте mapError. Он преобразует только значение ошибки, оставляя успешный результат и его тип без изменений.

Это удобно при переходе между слоями: инфраструктурная ошибка превращается в доменную, но успешное значение передаётся дальше как есть.

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

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

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

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

Пусть операция возвращает Result<Int, TransportError>, но публичный API должен возвращать Result<Int, DomainError>. Нельзя просто присвоить один результат другому: тип ошибки является частью типа Result.

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

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

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

enum TransportError: Error { case timeout } enum DomainError: Error { case serviceUnavailable } let source: Result<Int, TransportError> = .failure(.timeout) let domain = source.mapError { error in switch error { case .timeout: return DomainError.serviceUnavailable } }

После преобразования domain имеет тип Result<Int, DomainError>. Значение success осталось бы тем же Int, а failure(.timeout) превратился бы в failure(.serviceUnavailable).

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

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

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

Сетевой репозиторий возвращает ошибки транспорта: тайм-аут, отсутствие соединения и недопустимый ответ. Экрану приложения не нужны детали HTTP или сетевого стека; ему достаточно различать недоступность сервиса и некорректные данные.

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

Выбранное решение — выполнить mapError на границе слоя и преобразовать ошибки в доменный тип. Успешное значение не меняется, а классификация ошибок сосредоточена в одном месте. В результате верхний слой получает стабильный контракт и не зависит от деталей транспорта.

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

  1. Выполнится ли замыкание mapError, если Result содержит успех?

Нет. Замыкание вызывается только для значения в ветке failure. Успешное значение возвращается без преобразования, поэтому mapError безопасен для цепочек, где нужно адаптировать только ошибки.

  1. Может ли mapError превратить ошибку в успешный результат?

Нет. Он всегда возвращает Result с той же успешной веткой и преобразованной ошибочной веткой. Для восстановления после ошибки нужно явно разобрать результат и вернуть success, либо построить отдельную операцию преобразования с подходящей семантикой.

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

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