Result Type — тип-контејнер који представља резултат операције која се може завршити успехом (Success) или грешком (Failure). За разлику од изузетака, Result преноси грешку као обичну вредност без одмотавања стека, што чини обраду очекиваних грешака сигурнијом и композибилнијом. Према Apple Swift Documentation (2026), Result<Success, Failure> у Свифт-у омогућава комбиновање ланаца операција са аутоматском обрадом грешака кроз map и flatMap, без прекидања извршења програма.
Главно
Result Type је генерички тип који капсулира резултат извршења операције која се може завршити како успешно, тако и грешком. За разлику од изузетака, где грешка прекида нормални ток извршења и захтева одмотавање стека за проналажење catch, Result преноси грешку као обичну вредност — позивајућа страна увек добија објекат и сама одлучује како да поступи с њим. Ово је посебно корисно за очекиване грешке: неважећи унос, пословна правила, одбијање сервера, где би изузетак био превише тежак механизам.
Концепт Result потиче из функционалног програмирања, где се слични типови називају Either (или Left/Right). У Свифт-у, Result је постао стандардни тип у Свифт 5.0, у Котлин-у Result<T> се појавио у стандардној библиотеци, у Дарт 3.0 — уграђени Result<T>. Свака имплементација пружа методе за рад са контејнером: map (трансформација успешне вредности), flatMap (ланац Result функција), mapError (трансформација грешке), fold (обрада оба случаја). Result Type — камен темељац функционалне обраде грешака у мобилном развоју, алтернатива try-catch за очекиване сценарије.
Кључна предност Result-а је композибилност. Можете комбиновати више операција, од којих свака може вратити грешку, у један ланац без угнежђених try-catch. Ако било која операција у ланцу заврши са Failure, цео ланац се прекида и враћа Failure — без иједног услова if или блока catch. Ово чини код линеарним и читљивим, посебно у сценаријима са вишеструким секвенцијалним захтевима ка API-ју или проверама пословних правила.
У Свифт-у Result<Success, Failure> је еnum са два случаја: .success(Success) и .failure(Failure), где је Failure ограничен протоколом Error. Свифт Result — пуноправни функционални тип са методама map, flatMap, mapError и get. get — посебна метода: враћа вредност Success ако је резултат успешан, и баца грешку ако је Failure. Ово омогућава коришћење Result-а као моста између функционалног и стила заснованог на изузецима: обрађујте кроз map/flatMap у ланцу, а на крају — get кроз do-catch за интеграцију са throws кодом.
Свифт Result је еnum са генеричким параметрима, што омогућава компајлеру да провери исцрпну обраду кроз switch или do-catch. Ако у еnum NetworkError додате нови случај, компајлер ће дати грешку у свим switch изразима у којима овај случај није обрађен. Исцрпна провера (exhaustive checking) — главна предност Result-а над изузецима: компајлер гарантује да су све могуће грешке узете у обзир у фази компајлирања. Насупрот томе, изузеци се не проверавају од стране компајлера у Свифт-у (само 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 конкретан еnum са три случаја. Позивајућа страна обрађује сваки случај кроз switch са исцрпним покривањем — компајлер проверава да ли су све варијанте обрађене. Исцрпни switch — кључна предност Result-а над изузецима: компајлер гарантује да нећете заборавити да обрадите .badURL, .requestFailed или .decodingFailed. У случају изузетака, компајлер не захтева обраду, а необрађени catch(.badURL) лако се превиди у ревизији.
У Котлин-у Result<T> је уграђени тип из стандардне библиотеке, који представља успех (T) или грешку (Throwable). За разлику од Свифт-а, у Котлин-у Result не дозвољава навођење конкретног типа грешке — само Throwable. Ово је учињено ради једноставности, али захтева додатну проверу типа грешке кроз when израз. Котлин Result подржава функције fold (обрада оба случаја), getOrNull (успех или null), getOrDefault (успех или подразумевана вредност), map, recover, andThen. Посебност Котлин-а: Result није намењен директном пропагирању кроз границе функција — не може се користити као повратни тип за методе 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 — згодан омотач у Котлин-у који хвата било који изузетак и враћа Result.failure. validateEmail враћа Result.success или Result.failure у зависности од провере. fold обрађује оба случаја компактно: при Success-у се позива следећа функција валидације, при Failure-у — враћа се SignupResult.Error. Важно: Котлин Result није намењен чувању у пољима data class или директном преношењу кроз границе suspend функција — користите сопствене sealed class (Success/Error/Loading) за представљање UI стања у Jetpack Compose или MVVM.
Од Дарт 3.0 у стандардној библиотеци се појавио уграђени Result<T> — sealed class са два конструктора: T.ok() (успех) и Error.error() (грешка са Object и StackTrace). Пре Дарт 3.0, Флутер програмери су користили Either<L, R> из пакета dartz или сопствене sealed class. Уграђени Result у Дарт-у је минималистичан: не пружа 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 (подразумевана вредност). За Флутер апликације са функционалним приступом, 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 (успех). У Дарт 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 (такође назван 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 „Guest". Ако validateToken врати Left (InvalidToken), ланац се прекида и враћа се „Guest". Ако fetchProfile врати Left (UserNotFound) — такође „Guest". Ако обе операције буду успешне — име профила. flatMap омогућава комбиновање Either функција, од којих свака може да падне, у један линеарни ланац. У традиционалном стилу изузетака, исти би код захтевао два угнежђена try-catch или провере null. getOrElse на крају — тачка изласка из композиције, која пружа подразумевану вредност за случај Failure.
Result и изузеци — нису међусобно искључиви приступи. Сваки има своју област примене, и у добро дизајнираној мобилној апликацији користе се оба. Избор зависи од тога да ли је грешка очекивана (expected) или неочекивана (unexpected). Result — за очекиване грешке које су део пословне логике: неважећи емаил, недовољно средстава на рачуну, прекорачен лимит захтева. Изузеци — за неочекиване системске грешке: губитак мреже, OutOfMemoryError, NullPointerException (којих не би требало да буде, али се дешавају).
| Критеријум | Result Type | Изузеци (Exception/Error) |
|---|---|---|
| Тип грешке | Очекиване (пословна логика) | Неочекиване (системске) |
| Перформансе | Ниска цена (без одмотавања стека) | Висока цена (stack unwinding, хватање StackTrace) |
| Композиција | Кроз map/flatMap — линеарни ланци | Угнежђени try-catch — тешко за читање |
| Компајлер | Исцрпна провера (switch/when) | Само checked exceptions у Java |
| Ток извршења | Не прекида се — грешка као вредност | Прекида се до најближег catch |
| Тестирање | Лако: провера резултата, assert isSuccess/isError | Захтева assertThrows и mock објекте |
| Када користити | Пословна валидација, ланци захтева, форме | Губитак мреже, I/O грешке, падови система |
Практично правило: ако је грешка део нормалног тока рада апликације (корисник је унео неважећи емаил, нема довољно права приступа) — користите Result. Ако је грешка изузетна ситуација (сервер не одговара, понестало је меморије) — користите изузетке. У мобилном развоју Result на границама слојева (UseCase -> ViewModel) и изузеци унутар слојева (API -> Repository) — уобичајени образац који комбинује предности оба приступа.
Миграција постојећег кода заснованог на изузецима на 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 — када вам је потребна информација о грешци.
У Свифт-у користите Result { try throwingFunc() } — конструктор Result-а прихвата throws затварање. У Котлин-у — runCatching { throwingFunc() }, који враћа Result<T>. У Дарт-у — Result<T>.tryCatch(() => throwingFunc()). Ово омогућава лаку интеграцију кода заснованог на изузецима у Result ланце.
Користите mapError (Swift) или mapLeft (Either у Дарт/Котлин) за трансформацију типа грешке без промене успешне вредности. Ако треба да обрадите оба случаја и вратите јединствену вредност — користите fold. За евидентирање без прекидања ланца користите onFailure (Kotlin) или префиксни inspection point.
Да, али опрезно. Котлин Result<T> се не препоручује као повратни тип suspend функција директно због особености K2 компајлера и рефлексије. Користите сопствену 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође