The Result Type
The Swift standard library’s Result
type enables us to express the outcome of a given operation — whether it succeeded or failed — using a single, unified type. Let’s take a look at what kind of situations that Result
might be useful in, and a few tips and tricks that can be good to keep in mind when starting to work with that type.
While there are many different ways to model a Result
type, the one that comes built into the Swift standard library is declared as a generic enum that’s strongly typed for both the successful value that the result might contain, as well as for any error that was encountered. It looks like this:
enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
}
Like the above declaration shows, we can use Result
to represent any success/failure combination, as long as the Failure
type conforms to Swift’s Error
protocol. So how can we use the above type in practice, and what are the advantages of doing so?
As an example, let’s take a look at URLSession
, and one of its most commonly used APIs — which uses a closure-based design to return a network request’s various results in an asynchronous fashion:
let url = URL(string: "https://www.swiftbysundell.com")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
// Handle error
...
} else if let data = data {
// Handle successful response data
...
}
}
task.resume()
While URLSession
has evolved a lot over the years, and has an incredibly capable suite of APIs, deciding exactly how to handle the result of a network call can at times be a bit tricky — since, like the above example shows, both the data
and potential error
results are passed into our closure as optionals — which in turn requires us to unwrap each of those values every time we make a network call.
Let’s take a look at how using Result
could help us solve that problem. We’ll start by extending URLSession
with a new API that passes a Result<Data, Error>
value into its completion handler, rather than a group of optionals. To make that happen, we’ll unwrap the optionals that the standard API gives us (similar to what we do above) in order to construct our Result
— like this:
extension URLSession {
func dataTask(
with url: URL,
handler: @escaping (Result<Data, Error>) -> Void
) -> URLSessionDataTask {
dataTask(with: url) { data, _, error in
if let error = error {
handler(.failure(error))
} else {
handler(.success(data ?? Data()))
}
}
}
}
Note how we’ve simplified things a bit above, by ignoring the default API’s URLResponse
value (using an underscore instead of its parameter name within our closure). That’s not something that we might always want to do, although for simpler networking tasks, there might not be a need to inspect that response value.
If we now go back to our earlier call site and update it to use our new API, we can see that our code becomes a lot more clear — as we can now write completely separate code paths for both the success
and failure
case, like this:
let task = URLSession.shared.dataTask(with: url) { result in
switch result {
case .success(let data):
// Handle successful response data
...
case .failure(let error):
// Handle error
...
}
}
One interesting detail about the way we use Result
above is that we’ve specified its Failure
type simply as Error
. That means that any error can be passed into our result, which in turn limits our options for more specific error handling at the call site (since we don’t have any exhaustive list of potential errors to handle). While that’s tricky to change when working directly with a system API that, in turn, can throw any error — when we’re building a more specific form of abstraction we can often design a more unified error API for it.
For example, let’s say that we’re building a very simple image loader that’ll let us load an image over the network, again using URLSession
. But before we start actually implementing our loader itself, let’s first define an enum that lists all potential errors that it could encounter. For now, we’ll only have two cases — either a network error occurred, or the data that we downloaded turned out to be invalid:
enum ImageLoadingError: Error {
case networkFailure(Error)
case invalidData
}
Then, when building our image loader, we can now specialize Result
with the above error type — which in turn enables us to send much more rich error information to our call sites:
struct ImageLoader {
typealias Handler = (Result<UIImage, ImageLoadingError>) -> Void
var session = URLSession.shared
func loadImage(at url: URL,
then handler: @escaping Handler) {
let task = session.dataTask(with: url) { result in
switch result {
case .success(let data):
if let image = UIImage(data: data) {
handler(.success(image))
} else {
handler(.failure(.invalidData))
}
case .failure(let error):
handler(.failure(.networkFailure(error)))
}
}
task.resume()
}
}
The above design then enables us to handle each potential error in a much more granular fashion when using our image loader, for example like this:
let imageURL = URL(string: "https://www.swiftbysundell.com/images/logo.png")!
let imageLoader = ImageLoader()
imageLoader.loadImage(at: imageURL) { result in
switch result {
case .success(let image):
// Handle image
...
case .failure(.invalidData):
// Handle an invalid data failure
...
case .failure(.networkFailure(let error)):
// Handle any network error
...
}
}
Swift’s built-in Result
type might just take a handful of lines of code to declare, but the patterns that it enables us to adopt are really powerful, and can lead to much simpler code — especially when performing asynchronous operations, such as network calls.
To learn a lot more about result types in general, and some of the built-in type’s more powerful APIs, check out “The power of Result types in Swift”.
Thanks for reading! 🚀