At work I recently had to simplify API calls to an REST API. To do so we decided to use the Result<T,Error>
type which was introduced Swift 5. With this type it is possible to have a much more cleaner and easier error handling in Swift applications. Without further ado let us dive into code to see what it is actually like to use Result
.
Implementation
At first, a quick look at the beginning of the official implementation of the Result
type.
/// A value that represents either a success or a failure, including an
/// associated value in each case.
public 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)
// snip
}
It is nothing more then an enumeration with two cases: success
and failure
. The success case will contain a generic success value whereas the failure value will conform to the Error
protocol. There are additional methods defined on the Result
type but for now we are totally fine with the information we have right now - Result
is just an enum with two cases we can switch over.
Use it
In order to learn about the power of the Result
type, imagine you want to fetch the latest SpaceX launches from the open source SpaceX API. Without using Result
something like the following could be used:
func fetch() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("\(error)")
}
guard let response = response else {
print("Empty response")
return
}
guard let data = data else {
print("Empty data")
return
}
// Process your data now
}
}
First it is necessary to check if some error occurred. Then check if the response and the data are not nil. Only after we did all of the mentioned checks we are safe and can process our data. This is a little bit cumbersome isn't it ? Let us extend URLSession to take advantage of the Result
type.
Extending URLSession
extension URLSession {
func dataTask(with url: URL, result: @escaping (Result<(URLResponse, Data), Error>) -> Void) -> URLSessionDataTask {
return dataTask(with: url) { (data, response, error) in
if let error = error {
result(.failure(error))
return
}
guard let response = response, let data = data else {
let error = NSError(domain: "error", code: 0, userInfo: nil)
result(.failure(error))
return
}
result(.success((response, data)))
}
}
}
(Credit for this awesome extension goes to Alfian Losari, where I first got introduced to the Result
type.)
Basically what we do here is to wrap our original code into a way so that all layers above URLSession
can interact with the Result
, type instead of the original response types. Again the check for an error is necessary but now it is possible to return the .failure(error)
case. Then - again - check if data
and response
are not nil and if they do return another failure(error)
case. If everything went fine we simple return the success(data)
case, where our data is a tuple of the data
and response
. We did the hard work once and are now able to tremendously simplify the original fetch task.
Simplify
func fetch() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) { (result) in
switch result {
case .success(let (response, data)):
print("Do something with your data \(response, data)")
case .failure(let error):
print("Handle the \(error)")
}
}.resume()
}
With the extension in place it is possible to switch over the result
and react to the different cases accordingly. Much simpler than in our original implementation where we had to check for every possible source of failure.
Conclusion
The Result
types introduces a much cleaner API for handling errors. But it is not only restricted to API calls. You can use it in any situation were a method either returns a success value or can fail somehow. Your upper layers will be able to cleanly handling the error without the need of cumbersome checking beforehand.