Result is an elegant solution to what can be a syntactically complex problem, it enables us to work with asynchronous code in a strongly typed and meaningful way.

To fully understand Result, some knowledge of Generics is recommended

Proposed in SE-0235 and released with swift 5, Result is an enum with two cases - success and failure - that requires two types as type parameters. The first type parameter is used in the success case and the second passed into the failure case, the only condition here is that the second must conform to the Error protocol. Parameters of the provided generic types can be passed back through the success and failure cases as associated values, if you've not come across associated values in enums the Swift documentation has a dedicated section here, TL;DR you can think of it like passing a variable to a closure or function.

In our example we're going to setup the most common use case for Result, asynchronous network calls. Open up a playground and add the following struct

struct APIService {

    func callToServerSucceeds(urlString: String, 
        complete: @escaping (Result<String, Error>) -> Void) {

        complete(.success("Welcome to myserver.com!"))
    }

    func callToServerFails(urlString: String, 
        complete: @escaping (Result<String, Error>) -> Void) {

        let error = NSError(domain: "myserver.com", 
            code: 404, 
            userInfo: ["error": "Not found"])

        complete(.failure(error))
    }
}

This should all look fairly familiar if you've done any kind of network calls in Swift, the main difference here is that...and this is a big secret...we're not actually calling a server 🤫, instead we're just calling the passed in closure with some fake data to mimic an asynchronous network call.

you'll notice that our closure takes a single parameter - Result<String, Error>, the String type parameter will be used in conjunction with our success case as seen in callToServerSucceeds, and the Error type parameter used with our failure case in callToServerFails. We then go on to define the respective parameters in each of these functions, callToServerSucceeds passes .success("Welcome to myserver.com!") into its completion closure  and callToServerFails constructs a custom error and passes it into the failure case.

To see these functions in action, add the following code to the end of APIService

    
func callServer() {
    callToServerSucceeds(
    urlString: "https://www.myserver.com/message/") { (result) in
        self.handleResult(result)
    }

    callToServerFails(
    urlString: "https://www.myserver.com/bad/url/") { (result) in
        self.handleResult(result)
    }
}

func handleResult(_ result: Result<String, Error>) {
    switch result {
    case .success(let message):
        print(message)
    case .failure(let error):
        print(error.localizedDescription)
    }
}

Here we defined a new function to house our calls to our fake API, and passed the result to a new handleResult function which handles the two possible cases of our Result type. finally add these two lines to the bottom of your playground.

let service = APIService()
service.callServer()

Here we just instantiate an instance of APIService and call our function to trigger our fake network calls.

If you run this code you should see the success case print  our welcome message, and our failure case print a description of our custom error. At a basic level, that's all Result needs to do...

...but there's more.


Custom Error types

Result gives us the ability to pass in any type conforming to the Error protocol, this means we can define our own custom error types to make handling our network calls even more declarative.

Add the following enum at the top of the playground

enum NetworkError: Error {
    case badURL
    case invalidPayload
    case unreachable
}

Now we have an enum with three known cases, in reality there would likely be more, but for simplicity sake we'll keep it short.

Now in APIService replace Result<String, Error> in the function signature of callToServerFails and callToServerSucceeds with Result<String, NetworkError>, and replace  callToServerFails with the following

func callToServerFails(urlString: String, 
    complete: @escaping (Result<String, NetworkError>) -> Void) {	
    
    complete(.failure(.unreachable))
}

Notice we've replaced our NSError object with a case from our NetworkError enum! Lastly, we're going to update handleResult with  

func handleResult(_ result: Result<String, NetworkError>) {
    switch result {
    case .success(let message):
        print(message)
    case .failure(.badURL):
        print("bad url")
    case .failure(.invalidPayload):
        print("invalid payload")
    case .failure(.unreachable):
        print("unreachable")
    }
}

If you run the code again you should see "unreachable" print out in the console to match our .unreachable case, changing the case inside callToServerFails should print the corresponding message inside handleResult.


Wrap up

With the addition of Result, swift has changed the way we handle network code, previously there were two common patterns: use two separate closures - success and failure - with respective values, or a single closure containing optionals for success and failure cases (complete(result: [String: Any]?, failure: Error?)), meaning we have to nil check/optionally unwrap everythign - not ideal. While these approaches are still valid, Result gives us a succinct, strongly typed and 'swiftier' method for handling our asynchronous code moving forward.

Thanks for reading! ✌️