The Result enum is available since Swift 5 and allows us to define a success and failure case. The type is useful for defining the result of a failable operation in which we want to define both the value and error output type.
The standard Swift library adds more functionality to the result type which might be a bit lesser-known. Switching between both cases is a common pattern but we can leverage existing extensions to beautify our code, even more. Let’s dive into some code examples to show the available possibilities.
How to use the Result enum in Swift
Before we start using the Result enum it’s good to know its definition:
enum Result<Success, Failure> where Failure : Error {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
}
As you can see it has two defined cases:
- A success case which takes the generic
Success
type. This can be any type, includingVoid
- A failure case which takes the generic
Failure
type which has to conform to theError
protocol. In other words, it has to be an error type.
These two cases allow us to define a success and failure outcome of an operation that can fail.
Take the following example in which we define a method to fetch the even numbers from a given collection and a potential error type:
/// Define the potential error cases.
enum EvenNumberError: Error {
case emptyArray
}
/// A method capable of fetching even numbers from a given collection.
func evenNumbers(in collection: [Int]) -> Result<[Int], EvenNumberError> {
/// If the given collection is empty, return a failure instead.
guard !collection.isEmpty else {
return .failure(EvenNumberError.emptyArray)
}
/// The collection has items, fetch all even numbers.
let evenNumbers = collection.filter { number in number % 2 == 0 }
return .success(evenNumbers)
}
The method takes a collection of numbers as input and returns the result enum as a return value. Within the method, we first check whether the collection is filled. If not, we return the failure case with the correct emptyArray
error case. In case we found numbers, we return all even numbers.
Using this method looks as follows:
/// Create an array of numbers for our example.
let numbers: [Int] = [2,3,6,8,10]
let emptyArray = [Int]()
print(evenNumbers(in: emptyArray)) // Prints: failure(EvenNumberError.emptyArray)
print(evenNumbers(in: numbers)) // Prints: success([2, 6, 8, 10])
Passing in an empty array will give back a failure result while a collection of numbers is returned with even numbers only. This is a simple example of how you can make use of the result type.
A common pattern to use the result is by switching over the two cases:
switch evenNumbers(in: numbers) {
case .success(let evenNumbers):
print("Even numbers found: \(evenNumbers)")
case .failure(let error):
print("Fetching even numbers failed with \(error)")
}
The benefits of using a result return type:
- Define context by telling implementors of your method that it can fail
- The failure error type identifies the potential errors that can occur
- Instead of returning an optional
Error
and result value we can now simply switch two cases and get an unwrapped value
To clarify the last point I’d like to share you a code example how we would likely implement the above example without the Result enum:
func oldEvenNumbers(in collection: [Int]) -> (EvenNumberError?, [Int]?) {
/// If the given collection is empty, return a failure instead.
guard !collection.isEmpty else {
return (EvenNumberError.emptyArray, nil)
}
/// The collection has items, fetch all even numbers.
let evenNumbers = collection.filter { number in number % 2 == 0 }
return (nil, evenNumbers)
}
let evenNumbersResult = oldEvenNumbers(in: numbers)
if let error = evenNumbersResult.0 {
print(error)
} else if let result = evenNumbersResult.1 {
print(result)
}
Obviously, an extension on an array of integers would’ve been a better implementation but this is just to demonstrate the usage of the result enum. In fact, if you look at the current URLSession implementation, we get back a callback with both an optional error and data response:
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
It would’ve been much clearer if it was defined using the result enum:
func dataTask(with request: URLRequest, completionHandler: @escaping (_ result: Result<Data, Error>) -> Void) -> URLSessionDataTask
Enough reasons to understand that the Result enum is very useful in Swift.
Transforming a value
The Swift standard library defines methods on the Result enum to transform the result value. This includes both transforming the error and value type.
This is great for when you want to match multiple different error types into one:
enum CommonErrorType: Error {
case otherError(error: Error)
}
let result = evenNumbers(in: numbers).mapError { (evenNumberError) -> CommonErrorType in
return CommonErrorType.otherError(error: evenNumberError)
}
Or when you want to map the result value and return strings instead:
let evenNumberStringsResult = evenNumbers(in: numbers).map { (numbers) -> [String] in
return numbers.map { String($0) }
}
Sometimes, you want to map the result value using a method that can fail on its own. In this case, we can make use of the flatMap
method which allows us to map a Success
into a Failure
too:
let firstEvenNumberResult = evenNumbers(in: numbers).flatMap { (evenNumbers) -> Result<Int, EvenNumberError> in
guard let firstEvenNumber = evenNumbers.first else {
return .failure(EvenNumberError.emptyArray)
}
return .success(firstEvenNumber)
}
As the first
property can be nil
, we want to be able to return the Error.emptyArray
once again to catch that failure. The new return type defines a Success
value of a single integer, which represents the first even number if found.
We can do the same for the failure flow. In some cases, you want to return a default value when an operation fails. You can do this by making use of the flatMapError
method:
let fallbackEvenNumbers = [2,4,6,8,10]
let defaultNumbersResult = evenNumbers(in: []).flatMapError { (error) -> Result<[Int], EvenNumberError> in
if error == .emptyArray {
return .success(fallbackEvenNumbers)
}
return .failure(error)
}
print(defaultNumbersResult)
These transform methods are great for writing clean code, handling all potential flows in an operation that can fail.
Converting a throwing method into a Result enum
A common use-case is to convert an existing throwing method into a result type. This allows you to migrate methods you don’t control yourself, like 3rd party dependencies.
Take the following example of a throwing method, generating odd numbers of a collection:
func oddNumbers(in collection: [Int]) throws -> [Int] {
guard !collection.isEmpty else {
throw EvenNumberError.emptyArray
}
/// The collection has items, fetch all uneven numbers.
let oddNumbers = collection.filter { number in number % 2 == 1 }
return oddNumbers
}
We can use this throwing method in the result initialiser as follows:
let oddNumbersResult = Result { try oddNumbers(in: numbers) }
switch oddNumbersResult {
case .success(let oddNumbers):
print("Found odd numbers: \(oddNumbers)")
case .failure(let error):
print("Finding odd numbers failed with \(error)")
}
An error thrown by the oddNumbers(in:)
method will return a failure case while a successful fetch will generate a success case return value.
Converting a Result to a Throwing Expression
The other way around works too in which we can convert a Result into a throwing expression. Sometimes, you don’t want to explicitly handle both cases. For example, when you’re executing multiple throwing methods after each other.
In this case, you can use the get()
method which will either unwrap the success value or throw the inner failure error:
let numbers: [Int] = [2,3,6,8,10]
let evenNumbersResultValue = try evenNumbers(in: numbers).get()
print(evenNumbersResultValue) // Prints: 2, 6, 8, 10
Conclusion
The Result enum type in Swift is a readable way of defining two outcomes of an operation that can fail. It clarifies both a success and failure types which tells implementors what to expect. Besides switching over both cases you can make use of methods like map
, flatMap
, mapError
, and get()
to clean up your code, even more.
If you like to learn more tips on Swift, check out the swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!