Swift Result Type (Result<Success, Failure>): Complete Guide
The Result type is Swift’s standard way to represent an operation that either succeeds with a value or fails with an error. It is especially useful when you want to return success and failure information from a function, callback, or asynchronous API in a form that is easy to switch over and compose.
Quick answer: Result<Success, Failure> is an enum with two cases: .success(Success) and .failure(Failure). Use it when you want a typed, explicit way to return either a value or an error instead of throwing or using optional values.
Difficulty: Beginner to Intermediate
You'll understand this better if you know: basic Swift enums, error handling with throw and catch, and how functions return values.
1. What Is Result<Success, Failure>?
Result is a generic enum in the Swift standard library designed to model the outcome of an operation. It wraps either a successful value or a failure value, and both sides are part of the type signature.
- .success stores the successful output.
- .failure stores an error.
- The Success type can be almost anything.
- The Failure type must conform to Error.
This makes Result a safer alternative to “return a value or hope the caller notices the error somehow.”
2. Why Result Matters
Result matters because many Swift APIs need to communicate success and failure without immediately stopping execution. It is common in networking, file loading, parsing, and asynchronous callbacks.
Compared with using an optional, Result tells you why something failed. Compared with a thrown error, it can be easier to pass through closures, store, or transform later.
Use Result when you want:
- Explicit success and failure handling in one value.
- A clean way to pass results through completion handlers.
- Better composition with helper methods like map and flatMap.
- A single return type that documents what can happen.
3. Basic Syntax or Core Idea
The generic form is Result<Success, Failure>. Swift often infers the generic types, so you may also see just Result in context.
The two cases
Here is the minimal shape of a result value:
enum Result<Success, Failure : Error> {
case success(Success)
case failure(Failure)
}
In real Swift code, you do not define this yourself; it already exists in the standard library.
Creating a result value
You create a successful result with .success and a failing result with .failure.
let successResult = Result<String, Error>.success("Downloaded")
let failureResult = Result<String, Error>.failure(URLError(.notConnectedToInternet))
This gives you a value you can return, store, inspect, or transform later.
4. Step-by-Step Examples
Example 1: Returning a result from a function
Here is a simple parser that either returns a number or a custom error.
enum ParseError: Error {
case emptyInput
case invalidNumber
}
func parseScore(_ text: String) -> Result<Int, ParseError> {
guard !text.isEmpty else {
return .failure(.emptyInput)
}
guard let score = Int(text) else {
return .failure(.invalidNumber)
}
return .success(score)
}
This pattern makes the caller handle both the parsed value and the failure reason.
Example 2: Switching over the result
Callers usually use switch to handle both cases explicitly.
let result = parseScore("42")
switch result {
case .success(let value):
print("Parsed score:", value)
case .failure(let error):
print("Could not parse score:", error)
}
The compiler forces you to account for both paths, which is one of the main advantages of Result.
Example 3: Using map to transform success values
If the operation succeeds, you can transform the wrapped value without changing the failure case.
let parsed = parseScore("7")
let doubled = parsed.map { value in
value * 2
}
map only changes the success value. If the result is already a failure, it stays a failure.
Example 4: Converting a result into a thrown error
Sometimes you receive a result but want to integrate with throwing code.
func scoreValue(from text: String) throws -> Int {
switch parseScore(text) {
case .success(let value):
return value
case .failure(let error):
throw error
}
}
This is a common bridge when moving between callback-based APIs and throwing APIs.
5. Practical Use Cases
Result is most useful when you want to keep error information attached to the operation outcome.
- Networking callbacks that return downloaded data or an error.
- File loading utilities that may succeed with parsed content or fail with a readable error.
- Validation code that returns either a clean value or a reason it was rejected.
- Background work where you want to store the outcome and inspect it later.
- APIs that must avoid throws because the failure needs to travel through escaping closures.
It is less useful when failure is impossible, when a simple optional is enough, or when the caller should not proceed at all after an error.
6. Common Mistakes
Mistake 1: Using Result when an optional is enough
Sometimes developers use Result just to say “maybe there is a value.” That adds unnecessary complexity when there is no meaningful error to report.
Problem: If the only thing you can say is that the value is missing, wrapping it in Result makes the API harder to use without adding useful information.
func nickname(for name: String) -> Result<String, Error> {
if name.isEmpty {
return .failure(NSError(domain: "App", code: 1))
}
return .success(name)
}
Fix: Use an optional when you only need to represent presence or absence.
func nickname(for name: String) -> String? {
guard !name.isEmpty else {
return nil
}
return name
}
The corrected version is easier to read because it uses the simplest type that matches the problem.
Mistake 2: Ignoring the failure case
Some code only looks at the success path and forgets that a result can fail. This usually happens when developers try to extract the value too early.
Problem: Accessing only the success side without handling the failure leads to incomplete logic and often to compiler complaints when the result is not fully covered.
let result = parseScore("abc")
if case .success(let value) = result {
print(value)
}
Fix: Handle both cases with switch or provide an else branch.
switch parseScore("abc") {
case .success(let value):
print("Value:", value)
case .failure(let error):
print("Error:", error)
}
The corrected version works because it does not assume success is the only possible outcome.
Mistake 3: Using a non-Error failure type
The failure type must conform to Error. If it does not, Swift will reject the declaration.
Problem: A failure type like String does not satisfy the generic constraint, so the compiler cannot treat it as a valid Result failure.
let badResult: Result<Int, String> = .failure("Not allowed")
Fix: Define an error type that conforms to Error, or use an existing error type such as Error or URLError.
enum LoadError: Error {
case message(String)
}
let goodResult: Result<Int, LoadError> = .failure(.message("Not allowed"))
The corrected version compiles because the failure type now meets Swift’s requirement.
7. Best Practices
Practice 1: Use precise failure types
A specific error type makes your API clearer than a generic Error when you know the likely failure cases.
enum LoginError: Error {
case emptyUsername
case invalidPassword
}
This helps callers understand and handle failures more accurately.
Practice 2: Prefer switch when both outcomes matter
switch makes it obvious that both cases are handled.
switch parseScore("8") {
case .success(let value):
print(value)
case .failure(let error):
print(error)
}
This reads better than forcing the value out of the result too early.
Practice 3: Use result helpers for transformation
Methods like map, mapError, and flatMap keep pipelines clean.
let message = parseScore("12")
.map { value in "Score: \(value)" }
.mapError { error in "Parse error: \(error)" }
This keeps success and failure handling in one chain instead of branching repeatedly.
8. Limitations and Edge Cases
- Result is not a replacement for every optional return value.
- It can become verbose if you use it for very small, local operations.
- When bridging from older closure-based APIs, you may still see legacy callback styles that use separate value and error parameters.
- Result is great for one outcome and one failure, but less ideal for operations that need multiple partial errors.
- Some APIs prefer throwing because it fits the call site better, especially for synchronous work.
- A Result value does not automatically log, show, or recover from the error; you still need explicit handling.
A common “not working” scenario is assuming that Result itself prints a readable message. It only stores the outcome; formatting is up to your code.
9. Practical Mini Project
Let’s build a tiny account lookup service that returns a result instead of throwing. This example shows how Result keeps success and failure handling explicit.
struct User {
let id: Int
let name: String
}
enum LookupError: Error {
case notFound
case invalidID
}
func lookupUser(id text: String) -> Result<User, LookupError> {
guard let id = Int(text) else {
return .failure(.invalidID)
}
if id == 1 {
return .success(User(id: 1, name: "Ava"))
} else {
return .failure(.notFound)
}
}
let lookup = lookupUser(id: "1")
switch lookup {
case .success(let user):
print("Welcome, \(user.name)")
case .failure(let error):
switch error {
case .notFound:
print("User not found")
case .invalidID:
print("Please enter a numeric ID")
}
}
This mini project shows a typical pattern: parse input, return a typed success or failure, and handle both outcomes clearly at the call site.
10. Key Points
- Result<Success, Failure> stores either a success value or an error.
- The failure type must conform to Error.
- switch is the clearest way to handle both cases.
- map and related helpers make success transformations easier.
- Use Result when explicit outcome reporting is more useful than an optional or a thrown error.
11. Practice Exercise
- Write a function named divide that accepts two Double values.
- Return Result<Double, DivisionError>.
- Use one error case for division by zero.
- Print the quotient on success or the error on failure.
Expected output: If the denominator is zero, the program should report a division error. Otherwise, it should print the calculated quotient.
Hint: Check the denominator before dividing and return .failure early when it is zero.
enum DivisionError: Error {
case divisionByZero
}
func divide(_ lhs: Double, _ rhs: Double) -> Result<Double, DivisionError> {
guard rhs != 0 else {
return .failure(.divisionByZero)
}
return .success(lhs / rhs)
}
switch divide(10, 2) {
case .success(let value):
print("Quotient:", value)
case .failure(let error):
print("Division failed:", error)
}
12. Final Summary
Result<Success, Failure> is Swift’s built-in way to represent an operation that either produced a value or failed with an error. It is especially helpful when you want the outcome to stay explicit, type-safe, and easy to handle in callbacks or transformation pipelines.
Use it when the caller needs to know both the success value and the reason for failure. Prefer a simple optional when failure is not meaningful, and prefer throwing when synchronous code reads more naturally with try and catch.
Once you are comfortable switching over Result, the next step is to learn how to combine it with map, flatMap, and custom error types in real APIs.