Result Type — typ-kontener, który reprezentuje wynik operacji, która może zakończyć się sukcesem (Success) lub błędem (Failure). W przeciwieństwie do wyjątków, Result przekazuje błąd jako zwykłą wartość bez rozwijania stosu, co sprawia, że obsługa oczekiwanych błędów jest bezpieczniejsza i łatwiejsza do komponowania. Według Apple Swift Documentation (2026), Result<Success, Failure> w Swift pozwala łączyć łańcuchy operacji z automatyczną obsługą błędów przez map i flatMap, bez przerywania wykonania programu.
Najważniejsze
Result Type — to typ generyczny, który hermetyzuje wynik wykonania operacji, która może zakończyć się zarówno sukcesem, jak i błędem. W przeciwieństwie do wyjątków, gdzie błąd przerywa normalny przepływ wykonania i wymaga rozwijania stosu w celu znalezienia catch, Result przekazuje błąd jako zwykłą wartość — strona wywołująca zawsze otrzymuje obiekt i sama decyduje, jak go obsłużyć. Jest to szczególnie przydatne w przypadku oczekiwanych błędów: nieprawidłowe dane wejściowe, reguły biznesowe, odmowa serwera, gdzie wyjątek byłby zbyt ciężkim mechanizmem.
Koncepcja Result wywodzi się z programowania funkcyjnego, gdzie podobne typy nazywane są Either (lub Left/Right). W Swift Result stał się standardowym typem w Swift 5.0, w Kotlin Result<T> pojawił się w standardowej bibliotece, w Dart 3.0 — wbudowany Result<T>. Każda implementacja udostępnia metody do pracy z kontenerem: map (transformacja wartości sukcesu), flatMap (łańcuch funkcji Result), mapError (transformacja błędu), fold (obsługa obu przypadków). Result Type — kamień węgielny funkcyjnej obsługi błędów w programowaniu mobilnym, alternatywa dla try-catch dla oczekiwanych scenariuszy.
Kluczową zaletą Result jest komponowalność. Możesz łączyć wiele operacji, z których każda może zwrócić błąd, w jeden łańcuch bez zagnieżdżonych try-catch. Jeśli dowolna operacja w łańcuchu zakończy się Failure, cały łańcuch jest przerywany i zwraca Failure — bez ani jednego warunku if ani bloku catch. To sprawia, że kod jest liniowy i czytelny, szczególnie w scenariuszach z wieloma sekwencyjnymi zapytaniami do API lub sprawdzeniami reguł biznesowych.
W Swift Result<Success, Failure> — to enum z dwoma przypadkami: .success(Success) i .failure(Failure), gdzie Failure jest ograniczony protokołem Error. Swift Result — pełnoprawny typ funkcyjny z metodami map, flatMap, mapError i get. get — szczególna metoda: zwraca wartość Success, jeśli wynik jest pomyślny, i rzuca błąd, jeśli Failure. Pozwala to używać Result jako mostu między stylem funkcyjnym a opartym na wyjątkach: obsługuj przez map/flatMap w łańcuchu, a na końcu — get przez do-catch w celu integracji z kodem throws.
Swift Result to enum z parametrami generycznymi, co pozwala kompilatorowi sprawdzać wyczerpującą obsługę przez switch lub do-catch. Jeśli do enum NetworkError dodasz nowy przypadek, kompilator zgłosi błąd we wszystkich wyrażeniach switch, w których ten przypadek nie jest obsłużony. Wyczerpujące sprawdzanie (exhaustive checking) — główna zaleta Result nad wyjątkami: kompilator gwarantuje, że wszystkie możliwe błędy są uwzględnione na etapie kompilacji. W przeciwieństwie do tego, wyjątki nie są sprawdzane przez kompilator w Swift (tylko throws jest deklarowane, ale nie typ błędu).
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))
}
}
// Użycie z 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> pozwala typować błąd na poziomie typu: funkcja fetchUser zwraca Result<User, NetworkError>, gdzie NetworkError — konkretny enum z trzema przypadkami. Strona wywołująca obsługuje każdy przypadek przez switch z wyczerpującym pokryciem — kompilator sprawdza, czy wszystkie warianty są obsłużone. Wyczerpujący switch — kluczowa zaleta Result przed wyjątkami: kompilator gwarantuje, że nie zapomnisz obsłużyć .badURL, .requestFailed ani .decodingFailed. W przypadku wyjątków kompilator nie wymaga obsługi, a nieprzechwycony catch(.badURL) łatwo przeoczyć w review.
W Kotlin Result<T> — wbudowany typ ze standardowej biblioteki, reprezentujący sukces (T) lub błąd (Throwable). W przeciwieństwie do Swift, w Kotlin Result nie pozwala określić konkretnego typu błędu — tylko Throwable. Zrobiono to dla uproszczenia, ale wymaga dodatkowego sprawdzenia typu błędu przez wyrażenie when. Kotlin Result obsługuje funkcje fold (obsługa obu przypadków), getOrNull (sukces lub null), getOrDefault (sukces lub wartość domyślna), map, recover, andThen. Cechą szczególną Kotlin: Result nie jest przeznaczony do bezpośredniego propagowania przez granice funkcji — nie można go używać jako typu zwracanego dla metod Android SDK lub Kotlin Coroutines API bez dodatkowej adaptacji.
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 — wygodna otoczka w Kotlin, która przechwytuje dowolny wyjątek i zwraca Result.failure. validateEmail zwraca Result.success lub Result.failure w zależności od sprawdzenia. fold obsługuje oba przypadki zwięźle: przy Success wywoływana jest następna funkcja walidacji, przy Failure — zwracany jest SignupResult.Error. Ważne: Kotlin Result nie jest przeznaczony do przechowywania w polach data class ani do przekazywania przez granice suspend-funkcji bezpośrednio — używaj własnych sealed class (Success/Error/Loading) do reprezentowania stanów UI w Jetpack Compose lub MVVM.
Od Dart 3.0 w standardowej bibliotece pojawił się wbudowany Result<T> — sealed class z dwoma konstruktorami: T.ok() (sukces) i Error.error() (błąd z Object i StackTrace). Przed Dart 3.0 programiści Flutter używali Either<L, R> z pakietu dartz lub własnych sealed class. Wbudowany Result w Dart jest minimalistyczny: nie udostępnia map/flatMap bezpośrednio — te funkcje trzeba zaimplementować przez when lub użyć extension methods. Do poważnej funkcyjnej obsługi Either z fpdart pozostaje potężniejszym rozwiązaniem z obsługą map, flatMap, mapLeft, fold, andThen i operatorów bind.
Either<L, R> — lewostronnie skojarzony typ z pakietu fpdart, gdzie Left — błąd, Right — sukces. W przeciwieństwie do wbudowanego Result<T>, Either typuje błąd na poziomie parametru typu (L), co pozwala rozróżniać typy błędów na etapie kompilacji. Pakiet fpdart udostępnia pełny zestaw funkcyjnych kombinatorów: map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — zagnieżdżone Either), andThen (łańcuch bez transformacji), fold (wyjście z Either), getOrElse (wartość domyślna). Dla aplikacji Flutter z podejściem funkcyjnym Either jest standardem de facto.
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));
}
}
}
// Użycie z fold
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R> z fpdart — lewostronnie skojarzony typ: Left — błąd, Right — sukces. fetchUser zwraca Either<AppError, User>, gdzie AppError — sealed class z konkretnymi typami błędów (serverError, networkError). fold obsługuje oba przypadki: pierwsze callback dla Left (błąd), drugie dla Right (sukces). W Dart 3.0 wbudowany Result również obsługuje fold, ale nie udostępnia map/flatMap. Dla łańcuchów Either z fpdart udostępnia map, flatMap (bind), mapLeft, andThen — pełny zestaw funkcyjnych kombinatorów do kompozycji błędów.
Główną zaletą Result nad wyjątkami jest kompozycja. Jeśli masz kilka operacji, z których każda może zwrócić błąd, możesz połączyć je w łańcuch przez map i flatMap bez ani jednego zagnieżdżonego if lub try-catch. map transformuje wartość sukcesu: Result.success(x) -> Result.success(f(x)). flatMap (zwany również bind lub andThen) — dla przypadków, gdy transformacja sama zwraca Result: Result.success(x) -> f(x) -> Result<Y>. Jeśli na którymkolwiek kroku wystąpi Failure, kolejne operacje nie są wykonywane — łańcuch jest przerywany.
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))
// Kompozycja przez flatMap (andThen w fpdart)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
Łańcuch: validateToken -> fetchProfile -> map name -> getOrElse „Guest“. Jeśli validateToken zwróci Left (InvalidToken), łańcuch jest przerywany i zwracane jest „Guest“. Jeśli fetchProfile zwróci Left (UserNotFound) — również „Guest“. Jeśli obie operacje zakończą się sukcesem — nazwa profilu. flatMap pozwala łączyć funkcje Either, z których każda może upaść, w jeden liniowy łańcuch. W tradycyjnym stylu wyjątków ten sam kod wymagałby dwóch zagnieżdżonych try-catch lub sprawdzeń null. getOrElse na końcu — punkt wyjścia z kompozycji, który udostępnia wartość domyślną dla przypadku Failure.
Result i wyjątki — to nie wykluczające się podejścia. Każde ma swój obszar zastosowania i w dobrze zaprojektowanej aplikacji mobilnej używa się obu. Wybór zależy od tego, czy błąd jest oczekiwany (expected) czy nieoczekiwany (unexpected). Result — dla oczekiwanych błędów, które są częścią logiki biznesowej: nieprawidłowy email, niewystarczające środki na koncie, przekroczony limit zapytań. Wyjątki — dla nieoczekiwanych błędów systemowych: utrata sieci, OutOfMemoryError, NullPointerException (których nie powinno być, ale się zdarzają).
| Kryterium | Result Type | Wyjątki (Exception/Error) |
|---|---|---|
| Typ błędu | Oczekiwane (logika biznesowa) | Nieoczekiwane (systemowe) |
| Wydajność | Niski koszt (bez rozwijania stosu) | Wysoki koszt (stack unwinding, przechwytywanie StackTrace) |
| Kompozycja | Przez map/flatMap — liniowe łańcuchy | Zagnieżdżone try-catch — trudne do czytania |
| Kompilator | Wyczerpujące sprawdzanie (switch/when) | Tylko checked exceptions w Java |
| Przepływ wykonania | Nie jest przerywany — błąd jako wartość | Przerywany do najbliższego catch |
| Testowanie | Łatwe: sprawdzenie wyniku, assert isSuccess/isError | Wymaga assertThrows i obiektów mock |
| Kiedy używać | Walidacja biznesowa, łańcuchy zapytań, formularze | Utrata sieci, błędy I/O, awarie systemu |
Praktyczna zasada: jeśli błąd jest częścią normalnego przepływu pracy aplikacji (użytkownik wprowadził nieprawidłowy email, brak uprawnień dostępu) — używaj Result. Jeśli błąd to sytuacja wyjątkowa (serwer nie odpowiada, skończyła się pamięć) — używaj wyjątków. W programowaniu mobilnym Result na granicach warstw (UseCase -> ViewModel) i wyjątki wewnątrz warstw (API -> Repository) — to popularny wzorzec łączący zalety obu podejść.
Migracja istniejącego kodu opartego na wyjątkach na Result powinna być stopniowa. Zacznij od granic warstw: owiń wywołania funkcji throws w Result { try ... } (Swift) lub runCatching { ... } (Kotlin). Następnie zastąp typ zwracany metod Repository i UseCase na Result/Either, pozostawiając wewnętrzną implementację na wyjątkach. Na ostatnim etapie migruj ViewModel: zamiast UiState z wyjątkami używaj sealed class UiState<T> (Loading, Success, Error), gdzie Error przechowuje błąd domenowy, a nie Throwable. Stopniowa migracja pozwala testować każdą warstwę osobno bez globalnego refaktoryzowania.
Często zadawane pytania
Optional (T?) reprezentuje obecność lub brak wartości — nil oznacza „brak danych“, ale nie mówi dlaczego. Result (Success/Failure) zawiera nie tylko sukces, ale także przyczynę błędu z konkretnym typem. Używaj Optional, gdy brak wartości jest normą (np. opcjonalne pole profilu), Result — gdy potrzebujesz informacji o błędzie.
W Swift użyj Result { try throwingFunc() } — konstruktor Result przyjmuje throws-zamknięcie. W Kotlin — runCatching { throwingFunc() }, który zwraca Result<T>. W Dart — Result<T>.tryCatch(() => throwingFunc()). Pozwala to łatwo integrować kod oparty na wyjątkach w łańcuchy Result.
Użyj mapError (Swift) lub mapLeft (Either w Dart/Kotlin) do transformacji typu błędu bez zmiany wartości sukcesu. Jeśli trzeba obsłużyć oba przypadki i zwrócić jedną wartość — użyj fold. Do logowania bez przerywania łańcucha użyj onFailure (Kotlin) lub prefiksowego punktu inspekcji.
Tak, ale ostrożnie. Kotlin Result<T> nie jest zalecany jako typ zwracany suspend-funkcji bezpośrednio ze względu na cechy K2 compiler i refleksję. Używaj własnej sealed class NetworkResult<T> (Success, Error, Loading) do reprezentowania stanów w korutynach. Dla oczekiwanych błędów w logice biznesowej Either z Arrow — potężniejsza alternatywa.
fold — metoda, która przyjmuje dwa callbacki: onSuccess (dla przypadku sukcesu) i onFailure (dla przypadku błędu), i zwraca jedną wartość dowolnego typu. Jest to odpowiednik wyrażenia switch, ale w postaci funkcji wyższego rzędu. fold — główny punkt wyjścia z łańcuchów Result, gdzie przekształcasz Success/Failure w UiState, string dla użytkownika lub inny Result.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również