UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to use Result in Swift

Clear up any ambiguity and get typed errors too

Paul Hudson       @twostraws

SE-0235 introduced a Result type into the standard library, giving us a simpler, clearer way of handling errors in complex code such as asynchronous APIs. This is something folks have been asking for since the very earliest days of Swift, so it's great to see it finally arrived in Swift 5!

Swift’s Result type is implemented as an enum that has two cases: success and failure. Both are implemented using generics so they can have an associated value of your choosing, but failure must be something that conforms to Swift’s Error type. If you want, you can use a specific error type of your making, such as NetworkError or AuthenticationError, allowing us to have typed throws for the first time in Swift, but this isn't required.

To demonstrate Result, we could write a function that connects to a remote server to figure out how many unread messages are waiting for the user. In this example code we’re going to have just one possible error, which is that the requested URL string isn’t valid:

enum NetworkError: Error {
    case badURL
}

The fetching function will accept a URL string as its first parameter, and a completion handler as its second parameter. That completion handler will itself accept a Result, where the success case will store an integer saying how many unread messages there are, and the failure case will be some sort of NetworkError. We’re not actually going to connect to a server here, but using a completion handler at least lets us simulate asynchronous code – trying to return a value directly would cause the UI to freeze if we were doing real networking.

Here’s the code:

func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))
        return
    }

    // complicated networking code here 
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))
}

To use that code we need to check the value inside our Result to see whether our call succeeded or failed, like this:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}

Even in this simple scenario, Result has provided two benefits. First, the error we get back is now strongly typed: it must be some sort of NetworkError. Swift’s regular throwing functions are unchecked and so can throw any type of error. As a result, if you add a switch block to go over their cases you need to add default case even when it isn’t possible. With the strongly-typed errors of Result we can create exhaustive switch blocks by listing all the cases of our error enum.

Second, it’s now clear that we will get back either successful data or an error – it is not possible to get both or neither of them. You can see the importance of this second benefit if we rewrite fetchUnreadCount1() using the traditional, Objective-C approach to completion handlers:

func fetchUnreadCount2(from urlString: String, completionHandler: @escaping (Int?, NetworkError?) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler(nil, .badURL)
        return
    }

    print("Fetching \(url.absoluteString)...")
    completionHandler(5, nil)
}

Here the completion handler is expected to receive both an integer and an error, although either of them might be nil. Objective-C used this approach because it doesn’t have the ability to express enums with an associated value, so there was no choice but to send back both and let users figure it out at the call site.

However, that old approach means we’ve gone from two possible states to four: an integer with no error, an error with no integer, an error and an integer, and no error with no integer. Those last two ought to be impossible states, but there was no easy way to express this before Swift introduced Result.

This situation occurs a lot. The dataTask() method from URLSession uses the same approach, for example: it calls its completion handler with (Data?, URLResponse?, Error?). That might give us some data, a response, and an error, or any combination of the three – the Swift Evolution proposal calls this situation “awkwardly disparate”.

Think of Result as a super-powered Optional: it wraps a successful value, but can also wrap a second case that expresses the absence of a value. With Result, though, that absence conveys bonus data, because rather than just being nil it instead tells us what went wrong.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Why not use throws?

When you first meet Result it’s common to wonder why it’s useful, particularly when Swift already has a perfectly good throws keyword for handling errors ever since Swift 2.0.

You could implement much the same functionality by making the completion handler accept another function that throws or returns the data in question, like this:

func fetchUnreadCount3(from urlString: String, completionHandler: @escaping  (() throws -> Int) -> Void) {
    guard let url = URL(string: urlString) else {
        completionHandler { throw NetworkError.badURL }
        return
    }

    print("Fetching \(url.absoluteString)...")
    completionHandler { return 5 }
}

You could then call fetchUnreadCount3() with a completion handler that accepts a function to run, like this:

fetchUnreadCount3(from: "https://www.hackingwithswift.com") { resultFunction in
    do {
        let count = try resultFunction()
        print("\(count) unread messages.")
    } catch {
        print(error.localizedDescription)
    }
}

This gets us to more or less the same place, but it’s significantly more complicated to read. Worse, we don’t actually know what calling the result() function does, so there’s a risk it might cause its own problems if it does more than just send back a value or throw.

Even with simpler code, using throws often forces us handle errors immediately, rather than store them away for later processing. With Result that problem goes away: errors are stashed away in a value that we can read when we’re ready.

Working with Result

We’ve already looked at how switch blocks let us evaluate both the success and failure cases of Result in a clean way, but there are five more things you ought to know before you start using it.

First, Result has a get() method that either returns the successful value if it exists, or throws its error otherwise. This allows you to convert Result into a regular throwing call, like this:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if let count = try? result.get() {
        print("\(count) unread messages.")
    }
}

Second, you can use regular if statements to read the cases of an enum if you prefer, although some find the syntax a little odd at first. For example:

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if case .success(let count) = result {
        print("\(count) unread messages.")
    }
}

Third, Result has an initializer that accepts a throwing closure: if the closure returns a value successfully that gets used for the success case, otherwise the thrown error is placed into the failure case.

For example:

let result = Result { try String(contentsOfFile: someFile) }

Fourth, rather than using a specific error enum that you’ve created, you can also use the general Error protocol. In fact, the Swift Evolution proposal says “it's expected that most uses of Result will use Swift.Error as the Error type argument.”

So, rather than using Result<Int, NetworkError> you could use Result<Int, Error>. Although this means you lose the safety of typed throws, you gain the ability to throw a variety of different error enums – which you use really depends on your preferred coding style.

Finally, if you already have a custom Result type in your project – anything you have defined yourself or imported from one of the custom Result types on GitHub – then they will automatically be used in place of Swift’s own Result type. This will allow you to upgrade to Swift 5.0 without breaking your code, but ideally you’ll move to Swift’s own Result type over time to avoid incompatibilities with other projects.

Transforming Result

Result has four other methods that may prove useful: map(), flatMap(), mapError(), and flatMapError(). Each of these give you the ability to transform either the success or error somehow, and the first two work similarly to the methods of the same name on Optional.

The map() method looks inside the Result, and transforms the success value into a different kind of value using a closure you specify. However, if it finds failure instead, it just uses that directly and ignores your transformation.

To demonstrate this, we’re going to write some code that generates random numbers between 0 and a maximum then calculate the factors of that number. If the user requests a random number below zero, or if the number happens to be prime – i.e., it has no factors except itself and 1 – then we’ll consider those to be failures.

We might start by writing code to model the two possible failure cases: the user has tried to generate a random number below 0, and the number that was generated was prime:

enum FactorError: Error {
    case belowMinimum
    case isPrime
}

Next, we’d write a function that accepts a maximum number, and returns either a random number or an error:

func generateRandomNumber(maximum: Int) -> Result<Int, FactorError> {
    if maximum < 0 {
        // creating a range below 0 will crash, so refuse
        return .failure(.belowMinimum)
    } else {
        let number = Int.random(in: 0...maximum)
        return .success(number)
    }
}

When that’s called, the Result we get back will either be an integer or an error, so we could use map() to transform it:

let result1 = generateRandomNumber(maximum: 11)
let stringNumber = result1.map { "The random number is: \($0)." }

As we’ve passed in a valid maximum number, result will be a success with a random number. So, using map() will take that random number, use it with our string interpolation, then return another Result type, this time of the type Result<String, FactorError>.

However, if we had used generateRandomNumber(maximum: -11) then result would be set to the failure case with FactorError.belowMinimum. So, using map() would still return a Result<String, FactorError>, but it would have the same failure case and same FactorError.belowMinimum error.

Now that you’ve seen how map() lets us transform the success type to another type, let’s continue: we have a random number, so the next step is to calculate the factors for it. To do this, we’ll write another function that accepts a number and calculates its factors. If it finds the number is prime it will send back a failure Result with the isPrime error, otherwise it will send back the number of factors.

Here’s that in code:

func calculateFactors(for number: Int) -> Result<Int, FactorError> {
    let factors = (1...number).filter { number % $0 == 0 }

    if factors.count == 2 {
        return .failure(.isPrime)
    } else {
        return .success(factors.count)
    }
}

If we wanted to use map() to transform the output of generateRandomNumber() using calculateFactors(), it would look like this:

let result2 = generateRandomNumber(maximum: 10)
let mapResult = result2.map { calculateFactors(for: $0) }

However, that make mapResult a rather ugly type: Result<Result<Int, FactorError>, FactorError>. It’s a Result inside another Result.

Just like with optionals, this is where the flatMap() method comes in. If your transform closure returns a Result, flatMap() will return the new Result directly rather than wrapping it in another Result:

let flatMapResult = result2.flatMap { calculateFactors(for: $0) }

So, where mapResult was a Result<Result<Int, FactorError>, FactorError>, flatMapResult is flattened down into Result<Int, FactorError> – the first original success value (a random number) was transformed into a new success value (the number of factors). Just like map(), if either Result was a failure, flatMapResult will also be a failure.

As for mapError() and flatMapError(), those do similar things except they transform the error value rather than the success value.

What next?

We've yet to see Apple adopt Result in its own frameworks, presumably because of the need to retain Objective-C compatibility for as long as possible. However, many third-party libraries have picked it up, so you'll see it increasingly in coming months.

I’ve written articles about some of the other power features that were introduced alongside Result, and you might want to check them out:

You might also want to try out my What’s new in Swift 5.0 playground, which lets you try Swift 5’s new features interactively.

If you’re curious to learn more about result types in Swift, you might want to look over the source code for antitypical/Result on GitHub, which was one of the most popular result implementations before Swift implemented it directly.

I would also highly recommend reading Matt Gallagher’s excellent discussion of Result – it’s a few years old now, but still both useful and interesting.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.