Result Type — a container type that represents the result of an operation that can complete with success (Success) or failure (Failure). Unlike exceptions, Result passes the error as a regular value without stack unwinding, making expected error handling safer and more composable. According to Apple Swift Documentation (2026), Result<Success, Failure> in Swift allows chaining operations with automatic error handling through map and flatMap without interrupting program execution.
Key Takeaways
Result Type is a generic type that encapsulates the outcome of an operation that can either succeed or fail. Unlike exceptions, where an error interrupts the normal flow and requires stack unwinding to find a catch block, Result passes the error as a regular value — the caller always receives an object and decides how to handle it. This is especially useful for expected errors: invalid input, business rules, server rejection, where exceptions would be too heavy a mechanism.
The concept of Result originates from functional programming, where similar types are called Either (or Left/Right). In Swift, Result became a standard type in Swift 5.0; in Kotlin, Result<T> appeared in the standard library; in Dart 3.0, built-in Result<T> was introduced. Each implementation provides methods for working with the container: map (transform the success value), flatMap (chain Result functions), mapError (transform the error), fold (handle both cases). Result Type is a cornerstone of functional error handling in mobile development, an alternative to try-catch for expected scenarios.
The key advantage of Result is composability. You can combine multiple operations, each of which may fail, into a single chain without nested try-catch blocks. If any operation in the chain fails with Failure, the entire chain is interrupted and returns Failure — without a single if condition or catch block. This makes the code linear and readable, especially in scenarios with multiple sequential API requests or business rule checks.
In Swift, Result<Success, Failure> is an enum with two cases: .success(Success) and .failure(Failure), where Failure is constrained by the Error protocol. Swift Result is a fully functional type with map, flatMap, mapError, and get methods. get is a special method: it returns the Success value if the result is successful, and throws the error if it is Failure. This allows Result to act as a bridge between functional and exception-based styles: handle through map/flatMap in a chain, and at the end use get with do-catch to integrate with throws code.
Swift Result is an enum with generic parameters, which allows the compiler to enforce exhaustive handling through switch or do-catch. If you add a new case to NetworkError enum, the compiler will produce an error in all switch expressions where that case is not handled. Exhaustive checking is the main advantage of Result over exceptions: the compiler guarantees that all possible errors are accounted for at build time. In contrast, exceptions are not checked by the compiler in Swift (only throws is declared, but not the error type).
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))
}
}
// Usage with 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> allows typing the error at the type level: the fetchUser function returns Result<User, NetworkError>, where NetworkError is a concrete enum with three cases. The caller handles each case via switch with exhaustive coverage — the compiler checks that all variants are handled. Exhaustive switch is a key advantage of Result over exceptions: the compiler guarantees you won’t forget to handle .badURL, .requestFailed, or .decodingFailed. With exceptions, the compiler does not require handling, and a missing catch(.badURL) can easily slip through code review.
In Kotlin, Result<T> is a built-in type from the standard library that represents success (T) or error (Throwable). Unlike Swift, Kotlin Result does not allow specifying a concrete error type — only Throwable. This is done for simplicity but requires additional error type checking via when expression. Kotlin Result supports fold (handle both cases), getOrNull (success or null), getOrDefault (success or default), map, recover, andThen. A peculiarity of Kotlin: Result is not intended for direct propagation across function boundaries — it cannot be used as a return type for Android SDK methods or Kotlin Coroutines API without additional adaptation.
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 is a convenient wrapper in Kotlin that catches any exception and returns Result.failure. validateEmail returns Result.success or Result.failure depending on validation. fold handles both cases compactly: on Success, the next validation function is called; on Failure, SignupResult.Error is returned. Important: Kotlin Result is not intended for storage in data class fields or direct transmission across suspend function boundaries — use custom sealed classes (Success/Error/Loading) to represent UI states in Jetpack Compose or MVVM.
Since Dart 3.0, the standard library includes a built-in Result<T> — a sealed class with two constructors: T.ok() (success) and Error.error() (error with Object and StackTrace). Before Dart 3.0, Flutter developers used Either<L, R> from the dartz package or custom sealed classes. The built-in Result in Dart is minimalistic: it does not provide map/flatMap directly — these functions need to be implemented via when or using extension methods. For serious functional handling, Either from fpdart remains a more powerful solution with support for map, flatMap, mapLeft, fold, andThen, and bind operators.
Either<L, R> is a left-biased type from the fpdart package, where Left is the error and Right is the success. Unlike the built-in Result<T>, Either types the error at the type parameter level (L), allowing error type differentiation at compile time. The fpdart package provides a full set of functional combinators: map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — nested Either), andThen (chain without transformation), fold (exit from Either), getOrElse (default value). For Flutter applications with a functional approach, Either is the de facto standard.
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));
}
}
}
// Usage with fold
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R> from fpdart is a left-biased type: Left — error, Right — success. fetchUser returns Either<AppError, User>, where AppError is a sealed class with concrete error types (serverError, networkError). fold handles both cases: the first callback for Left (error), the second for Right (success). In Dart 3.0, the built-in Result also supports fold, but does not provide map/flatMap. For chains, Either from fpdart provides map, flatMap (bind), mapLeft, andThen — a full set of functional combinators for error composition.
The main advantage of Result over exceptions is composition. If you have multiple operations, each of which may fail, you can chain them using map and flatMap without a single nested if or try-catch. map transforms the success value: Result.success(x) -> Result.success(f(x)). flatMap (also called bind or andThen) is for cases where the transformation itself returns a Result: Result.success(x) -> f(x) -> Result<Y>. If any step fails with Failure, subsequent operations are not executed — the chain is interrupted.
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))
// Composition via flatMap (andThen in fpdart)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
Chain: validateToken -> fetchProfile -> map name -> getOrElse “Guest”. If validateToken returns Left (InvalidToken), the chain is interrupted and returns “Guest”. If fetchProfile returns Left (UserNotFound) — also “Guest”. If both operations succeed — the profile name. flatMap allows combining Either functions, each of which may fail, into a single linear chain. In the traditional exception style, the same code would require two nested try-catch or null checks. getOrElse at the end is the exit point from the composition, providing a default value for the Failure case.
Result and exceptions are not mutually exclusive approaches. Each has its own domain, and in a well-designed mobile application, both are used. The choice depends on whether the error is expected or unexpected. Result is for expected errors that are part of business logic: invalid email, insufficient funds, rate limit exceeded. Exceptions are for unexpected system errors: network loss, OutOfMemoryError, NullPointerException (which should not happen but do occur).
| Criterion | Result Type | Exceptions (Exception/Error) |
|---|---|---|
| Error Type | Expected (business logic) | Unexpected (system) |
| Performance | Low cost (no stack unwinding) | High cost (stack unwinding, StackTrace capture) |
| Composition | Via map/flatMap — linear chains | Nested try-catch — hard to read |
| Compiler | Exhaustive checking (switch/when) | Only checked exceptions in Java |
| Execution Flow | Not interrupted — error as a value | Interrupted until nearest catch |
| Testing | Easy: check result, assert isSuccess/isError | Requires assertThrows and mock objects |
| When to Use | Business validation, request chains, forms | Network loss, I/O errors, system crashes |
Practical rule: if the error is part of the normal application workflow (user entered an invalid email, insufficient permissions) — use Result. If the error is an exceptional situation (server not responding, out of memory) — use exceptions. In mobile development, Result at layer boundaries (UseCase -> ViewModel) and exceptions within layers (API -> Repository) is a common pattern that combines the advantages of both approaches.
Migrating existing exception-based code to Result should be gradual. Start with layer boundaries: wrap throws function calls in Result { try ... } (Swift) or runCatching { ... } (Kotlin). Then replace the return type of Repository and UseCase methods with Result/Either, keeping the internal implementation on exceptions. In the final step, migrate the ViewModel: instead of UiState with exceptions, use a sealed class UiState<T> (Loading, Success, Error), where Error stores a domain error, not a Throwable. Gradual migration allows testing each layer separately without a global refactoring.
Frequently Asked Questions
Optional (T?) represents the presence or absence of a value — nil means “no data” but does not explain why. Result (Success/Failure) contains not only success but also the error reason with a concrete type. Use Optional when absence is normal (e.g., an optional profile field), and Result when you need error information.
In Swift, use Result { try throwingFunc() } — the Result constructor takes a throwing closure. In Kotlin, use runCatching { throwingFunc() }, which returns Result<T>. In Dart, use Result<T>.tryCatch(() => throwingFunc()). This allows easy integration of exception-based code into Result chains.
Use mapError (Swift) or mapLeft (Either in Dart/Kotlin) to transform the error type without changing the success value. If you need to handle both cases and return a single value, use fold. For logging without interrupting the chain, use onFailure (Kotlin) or a prefix inspection point.
Yes, but with caution. Kotlin Result<T> is not recommended as a return type for suspend functions directly due to K2 compiler and reflection peculiarities. Use your own sealed class NetworkResult<T> (Success, Error, Loading) to represent states in coroutines. For expected errors in business logic, Either from Arrow is a more powerful alternative.
fold is a method that takes two callbacks: onSuccess (for the success case) and onFailure (for the error case), and returns a single value of any type. It is the equivalent of a switch expression but as a higher-order function. fold is the main exit point from Result chains, where you convert Success/Failure into UiState, a user-facing string, or another Result.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also