NEW: Nominations are now open for the 2019 Swift Community Awards! >>

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.

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.

SAVE 20% ON iOS CONF SG The largest iOS conference in Southeast Asia is back in Singapore for the 5th time in January 2020, now with two days of workshops plus two days of talks on SwiftUI, Combine, GraphQL, and more! Save a massive 20% on your tickets by clicking on this link.

MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and a speaker at Swift events around the world. If you're curious you can learn more here.

Was this page useful? Let us know!

Average rating: 4.7/5