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 — наріжний камінь функціональної обробки помилок у мобільній розробці, альтернатива 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 або 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 «Гість». Якщо validateToken поверне Left (InvalidToken), ланцюжок переривається і повертається «Гість». Якщо fetchProfile поверне Left (UserNotFound) — теж «Гість». Якщо обидві операції успішні — ім’я профілю. 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 |
| Тестування | Легко: перевірка результату, 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також