Result Type — тип-контейнер, который представляет результат операции, которая может завершиться успехом (Success) или ошибкой (Failure). В отличие от исключений, Result передаёт ошибку как обычное значение без раскрутки стека, что делает обработку ожидаемых ошибок более безопасной и композируемой. По данным Apple Swift Documentation (2026), Result<Success, Failure> в Swift позволяет объединять цепочки операций с автоматической обработкой ошибок через map и flatMap, не прерывая выполнение программы.
Главное
Result Type — это generic-тип, инкапсулирующий результат выполнения операции, которая может завершиться как успешно, так и с ошибкой. В отличие от исключений, где ошибка прерывает нормальный поток выполнения и требует раскрутки стека для поиска catch, Result передаёт ошибку как обычное значение — вызывающая сторона всегда получает объект и сама решает, как с ним поступить. Это особенно полезно для ожидаемых ошибок: неверный ввод, бизнес-правила, отказ сервера, где исключение было бы слишком тяжёлым механизмом.
Концепция Result восходит к функциональному программированию, где подобные типы называются Either (или Left/Right). В Swift Result стал стандартным типом в Swift 5.0, в Kotlin Result<T> появился в стандартной библиотеке, в Dart 3.0 — встроенный Result<T>. Каждая реализация предоставляет методы для работы с контейнером: map (трансформация успешного значения), flatMap (цепочка Result-функций), mapError (трансформация ошибки), fold (обработка обоих кейсов). Result Type — краеугольный камень functional error handling в мобильной разработке, альтернатива try-catch для ожидаемых сценариев.
Ключевое преимущество Result — композируемость. Вы можете объединять несколько операций, каждая из которых может вернуть ошибку, в одну цепочку без вложенных try-catch. Если любая операция в цепочке завершается Failure, вся цепочка прерывается и возвращает Failure — без единого условия if или блока catch. Это делает код линейным и читаемым, особенно в сценариях с множественными последовательными запросами к API или проверками бизнес-правил.
В Swift Result<Success, Failure> — это enum с двумя кейсами: .success(Success) и .failure(Failure), где Failure ограничен протоколом Error. Swift Result — полноценный функциональный тип с методами map, flatMap, mapError и get. get — особый метод: он возвращает значение Success, если результат успешный, и выбрасывает ошибку, если Failure. Это позволяет использовать Result как мост между функциональным и exception-based стилями: обрабатывай через map/flatMap в цепочке, а в конце — get через do-catch для интеграции с throws-кодом.
Swift Result — это enum с generic параметрами, что позволяет компилятору проверять исчерпывающую обработку через switch или do-catch. Если в enum NetworkError добавить новый кейс, компилятор выдаст ошибку во всех switch-выражениях, где этот кейс не обработан. Exhaustive checking — главное преимущество Result перед исключениями: компилятор гарантирует, что все возможные ошибки учтены на этапе сборки. В отличие от этого, исключения не проверяются компилятором в Swift (только throws декларируется, но не тип ошибки).
enum NetworkError: Error {
case badURL
case requestFailed(String)
case decodingFailed
}
func fetchUser(id: Int) -> Result<User, NetworkError> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return .failure(.badURL)
}
let result = performRequest(url: url)
switch result {
case let .success(data):
if let user = try? JSONDecoder().decode(User.self, from: data) {
return .success(user)
}
return .failure(.decodingFailed)
case let .failure(error):
return .failure(.requestFailed(error.localizedDescription))
}
}
// Использование с switch
let result = fetchUser(id: 42)
switch result {
case .success(let user):
showUser(user)
case .failure(.badURL):
logError("Invalid URL")
case .failure(.requestFailed(let msg)):
showAlert(msg)
case .failure(.decodingFailed):
logError("Decoding error")
}
Result<Success, Failure> позволяет типизировать ошибку на уровне типа: функция fetchUser возвращает Result<User, NetworkError>, где NetworkError — конкретный enum с тремя кейсами. Вызывающая сторона обрабатывает каждый кейс через switch с исчерпывающим покрытием — компилятор проверяет, что все варианты обработаны. Exhaustive switch — ключевое преимущество Result перед исключениями: компилятор гарантирует, что вы не забудете обработать .badURL, .requestFailed или .decodingFailed. В случае с исключениями компилятор не требует обработки, и незадествованный catch(.badURL) легко пропустить в ревью.
В Kotlin Result<T> — встроенный тип из стандартной библиотеки, представляющий успех (T) или ошибку (Throwable). В отличие от Swift, в Kotlin Result не позволяет указать конкретный тип ошибки — только Throwable. Это сделано для простоты, но требует дополнительной проверки типа ошибки через when-выражение. Kotlin Result поддерживает функции fold (обработка обоих кейсов), getOrNull (успех или null), getOrDefault (успех или default), map, recover, andThen. Особенность Kotlin: Result не предназначен для прямого propagation через границы функций — его нельзя использовать как возвращаемый тип для методов Android SDK или Kotlin Coroutines API без дополнительной адаптации.
fun parseJson(input: String): Result<JsonObject> {
return runCatching {
JsonParser.parseString(input).asJsonObject()
}
}
fun validateEmail(email: String): Result<String> {
return if (email.contains("@")) {
Result.success(email.trim())
} else {
Result.failure(IllegalArgumentException("Invalid email"))
}
}
data class SignupData(val name: String, val email: String)
fun processSignup(name: String, email: String): SignupResult {
return validateEmail(email).fold(
onSuccess = { validateName(name) },
onFailure = { SignupResult.Error("Invalid email") }
)
}
runCatching — удобная обёртка в Kotlin, которая перехватывает любое исключение и возвращает Result.failure. validateEmail возвращает Result.success или Result.failure в зависимости от проверки. fold обрабатывает оба кейса компактно: при Success вызывается следующая функция валидации, при Failure — возвращается SignupResult.Error. Важно: Kotlin Result не предназначен для хранения в полях data class или передачи через границы suspend-функций напрямую — используйте собственные sealed class (Success/Error/Loading) для представления состояний UI в Jetpack Compose или MVVM.
С Dart 3.0 в стандартной библиотеке появился встроенный Result<T> — sealed class с двумя конструкторами: T.ok() (успех) и Error.error() (ошибка с Object и StackTrace). До Dart 3.0 разработчики Flutter использовали Either<L, R> из пакета dartz или кастомные sealed class. Встроенный Result в Dart минималистичен: он не предоставляет map/flatMap напрямую — эти функции нужно реализовать через when или использовать extension methods. Для серьёзной функциональной обработки Either из fpdart остаётся более мощным решением с поддержкой map, flatMap, mapLeft, fold, andThen и операторов bind.
Either<L, R> — лево-ассоциированный тип из пакета fpdart, где Left — ошибка, Right — успех. В отличие от встроенного Result<T>, Either типизирует ошибку на уровне параметра типа (L), что позволяет различать типы ошибок на этапе компиляции. Пакет fpdart предоставляет полный набор функциональных комбинаторов: map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — вложенные Either), andThen (цепочка без трансформации), fold (выход из Either), getOrElse (значение по умолчанию). Для Flutter-приложений с функциональным подходом Either — стандарт де-факто.
import 'dart:convert';
import 'package:fpdart/fpdart.dart';
class UserService {
Either<AppError, User> fetchUser(String id) {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id')
);
if (response.statusCode == 200) {
final user = User.fromJson(
json.decode(response.body)
);
return Either.of(user);
}
return Either.left(
AppError.serverError(response.statusCode)
);
} on SocketException catch (e) {
return Either.left(AppError.networkError(e.message));
}
}
}
// Использование с fold
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R> из fpdart — лево-ассоциированный тип: Left — ошибка, Right — успех. fetchUser возвращает Either<AppError, User>, где AppError — sealed class с конкретными типами ошибок (serverError, networkError). fold обрабатывает оба кейса: первый callback для Left (ошибка), второй для Right (успех). В Dart 3.0 встроенный Result также поддерживает fold, но не предоставляет map/flatMap. Для цепочек Either из fpdart предоставляет map, flatMap (bind), mapLeft, andThen — полный набор функциональных комбинаторов для композиции ошибок.
Главное преимущество Result перед исключениями — композиция. Если у вас есть несколько операций, каждая из которых может вернуть ошибку, вы можете объединить их в цепочку через map и flatMap без единого вложенного if или try-catch. map трансформирует успешное значение: Result.success(x) -> Result.success(f(x)). flatMap (также called bind or andThen) — для случаев, когда трансформация сама возвращает Result: Result.success(x) -> f(x) -> Result<Y>. Если на любом шаге возникает Failure, последующие операции не выполняются — цепочка прерывается.
data class UserRequest(val userId: String, val token: String)
sealed class AuthError {
data object InvalidToken : AuthError()
data class UserNotFound(val id: String) : AuthError()
}
typealias Outcome<T> = Either<AuthError, T>
fun validateToken(token: String): Outcome<String> =
if (token.isNotBlank()) Either.right(token)
else Either.left(AuthError.InvalidToken)
fun fetchProfile(userId: String): Outcome<Profile> =
if (userId == "42") Either.right(Profile("Alice"))
else Either.left(AuthError.UserNotFound(userId))
// Композиция через flatMap (andThen в fpdart)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
Цепочка: validateToken -> fetchProfile -> map name -> getOrElse "Guest". Если validateToken вернёт Left (InvalidToken), цепочка прерывается и возвращается "Guest". Если fetchProfile вернёт Left (UserNotFound) — тоже "Guest". Если обе операции успешны — имя профиля. flatMap позволяет объединять Either-функции, каждая из которых может упасть, в одну линейную цепочку. В традиционном exception-стиле тот же код потребовал бы двух вложенных try-catch или проверок null. getOrElse в конце — точка выхода из композиции, которая предоставляет значение по умолчанию для Failure-кейса.
Result и исключения — не взаимоисключающие подходы. Каждый имеет свою область применения, и в хорошо спроектированном мобильном приложении используются оба. Выбор зависит от того, является ли ошибка ожидаемой (expected) или неожиданной (unexpected). Result — для ожидаемых ошибок, которые являются частью бизнес-логики: неверный email, недостаточно средств на счету, превышен лимит запросов. Исключения — для неожиданных системных ошибок: потеря сети, OutOfMemoryError, NullPointerException (которых не должно быть, но они случаются).
| Критерий | Result Type | Исключения (Exception/Error) |
|---|---|---|
| Тип ошибки | Ожидаемые (бизнес-логика) | Неожиданные (системные) |
| Производительность | Низкая стоимость (без раскрутки стека) | Высокая стоимость (stack unwinding, захват StackTrace) |
| Композиция | Через map/flatMap — линейные цепочки | Вложенные try-catch — трудно читать |
| Компилятор | Exhaustive checking (switch/when) | Только checked exceptions в Java |
| Поток выполнения | Не прерывается — ошибка как значение | Прерывается до ближайшего catch |
| Тестирование | Легко: enough результата, assert isSuccess/isError | Требует assertThrows и mock-объектов |
| Когда использовать | Бизнес-валидация, цепочки запросов, формы | Потеря сети, ошибки I/O, падения системы |
Практическое правило: если ошибка — часть нормального потока работы приложения (пользователь ввёл неверный email, не хватает прав доступа) — используйте Result. Если ошибка — исключительная ситуация (сервер не отвечает, закончилась память) — используйте исключения. В мобильной разработке Result на границе слоёв (UseCase -> ViewModel) и исключения внутри слоёв (API -> Repository) — распространённый паттерн, сочетающий преимущества обоих подходов.
Миграция существующего exception-based кода на Result должна быть постепенной. Начните с границ слоёв: оберните вызовы throws-функций в Result { try ... } (Swift) или runCatching { ... } (Kotlin). Затем замените возвращаемый тип методов Repository и UseCase на Result/Either, оставив внутреннюю реализацию на исключениях. На последнем этапе мигрируйте ViewModel: вместо UiState с исключениями используйте sealed class UiState<T> (Loading, Success, Error), где Error хранит доменную ошибку, а не Throwable. Постепенная миграция позволяет тестировать каждый слой отдельно без глобального рефакторинга.
Часто задаваемые вопросы
Optional (T?) представляет наличие или отсутствие значения — nil означает "нет данных", но не говорит почему. Result (Success/Failure) содержит не только успех, но и причину ошибки с конкретным типом. Используйте Optional, когда отсутствие значения — норма (например, опциональное поле профиля), Result — когда нужна информация об ошибке.
В Swift используйте Result { try throwingFunc() } — конструктор Result принимает throws-замыкание. В Kotlin — runCatching { throwingFunc() }, который возвращает Result<T>. В Dart — Result<T>.tryCatch(() => throwingFunc()). Это позволяет легко интегрировать exception-based код в Result-цепочки.
Используйте mapError (Swift) или mapLeft (Either в Dart/Kotlin) для трансформации типа ошибки без изменения успешного значения. Если нужно обработать оба кейса и вернуть единое значение — используйте fold. Для логирования без прерывания цепочки используйте onFailure (Kotlin) или префиксный inspection point.
Да, но осторожно. Kotlin Result<T> не рекомендуется как возвращаемый тип suspend-функций напрямую из-за особенностей с K2 compiler и рефлексией. Используйте собственный sealed class NetworkResult<T> (Success, Error, Loading) для представления состояний в корутинах. Для ожидаемых ошибок в бизнес-логике Either из Arrow — более мощная альтернатива.
fold — метод, который принимает два callback: onSuccess (для успешного кейса) и onFailure (для ошибочного), и возвращает единое значение любого типа. Это эквивалент switch-выражения, но в виде функции высшего порядка. fold — основная точка выхода из Result-цепочек, где вы преобразуете Success/Failure в UiState, строку для пользователя или другой Result.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также