Episode #417

Swift 5's Result Type

16 minutes
Published on November 8, 2019
Before Swift 5 we used to write our own Result type to contain a value or an error (but never both). A lot of 3rd party libraries brought along their own as well. Then Swift 5 came and brought us Result. Not only is it slightly different than the ones we might be familiar with, Swift's Result type also has some useful functionality up its sleeve.

Episode Link:

  • FileManager.contentsOfDirectory - This is an example that shows the difference between Obj-C style NSError** style of reporting errors versus Swift's throws.

Example of an Asynchronous API

Using Result type of Swift 5, we'll handle the errors in asynchronous APIs.

We'll create an example of an asynchronous API with completion block and escaping closure having optional return values. Apart from the success and failure case, the function may receive both the optional values or they may be nil or empty.

```swift receive a success or
func getMessage(completion: @escaping (String?, Error?) -> Void) {

}
```

Using Result Type for Success

Using the Result type, we'll specify the return value received in the case of success and failure.

func getMessage(completion: @escaping (Result<String, Error>) -> Void) {
    completion(.success("It worked!"))
}

getMessage { result in
    switch result {
    case .success(let msg):
        print(String(msg))

    case .failure(let error):
        print(error)
    }
}

Using Result type for Failure

In case of failure, the type of returned value Error is needed to match the error type that conformance to the Error protocol.

struct MyCustomError : Error { }

func getMessageThatFails(completion: @escaping (Result<String, Error>) -> Void) {
    completion(.failure(MyCustomError()))
}

getMessageThatFails { result in
    switch result {
    case .success(let msg):
        print(msg)

    case .failure(let error):
        print(error)
    }
}

Transforming a Result Using Map

Result enables the transformation of success value into a new result. We'll use the map method that returns a new result by mapping any success value using the given transformation.

getMessage { result in
    let newResult = result.map { $0.reversed() }

    switch newResult2 {
    case .success(let msg):
        print(String(msg))

    case .failure(let error):
        print(error)
    }
}

Now we'll transform the error by mapping it to another custom error.

struct MyOtherError : Error { }

getMessage { result in
    let newResult = result
        .map { $0.reversed() }
        .mapError { _ in MyOtherError() }

    switch newResult {
    case .success(let msg):
        print(String(msg))

    case .failure(let error):
        print(error)
    }
}

Note that this changes the type of Result we are working with.

Transforming a Result Using FlatMap

Another way of transforming a Result is by using flatMap. This allows us to convert a successful value into an Error. In this example, imagine that we want to throw an error if the message has an odd number of characters.

struct OddNumberOfCharacters : Error { }

getMessage { result in
    let newResult = result
        .map { $0.reversed() }

    // MAP Result<T, E>   T->U   --> Result<U, E>
    // flatMap -> Result<U, E>
    // Result<T, E>      T->Result<U, E>   --> Result<U, E>    

    let newResult2 = newResult.flatMap { msg -> Result<String, Error> in
        if msg.count % 2 == 0 {
            return .success(String(msg))
        } else {
            return .failure(OddNumberOfCharacters())
        }
    }
}

Convert a Result to Throw an Expression

We can convert a result type into a throwing style by using get():

let r: Result<Int, Error> = .success(52)

do {
    try r.get()
} catch {
    // ...
}

We can use get along with try? to only return the success value instead of the exception.

let r: Result<Int, Error> = .success(52)

let x = try? r.get()

gaurd let x = try? r.get() else { return}

if let x = try? r.get() {

}

// or if we are absolutely 
try! r.get()

Chaining the Result of an Operation Into Another

To convert an input of an operation into another, we'll create 3 operations returning the success and the custom errors. We'll use flatMap to convert the result of one operation as an input of the next operation.

enum CustomErrors : String, Error, CustomStringConvertible {
    case operation1Failed
    case operation2Failed
    case operation3Failed

    var description: String {
        return "🔥 \(self.rawValue)"
    }
}

func operation1() -> Result<Int, CustomErrors> {
    print("Running operation 1")
    let random = Int.random(in: 1...10)
    if random < 3 {
        return .failure(.operation1Failed)
    }
    return .success(random)
}

func operation2(_ input: Int) -> Result<String, CustomErrors> {
    print("Running operation 2")
    if Bool.random() {
        return .failure(.operation2Failed)
    } else {
        var s = ""
        for _ in 1...input {
            s.append("🤓")
        }
        return .success(s)
    }
}

func operation3(_ input: String) -> Result<String, CustomErrors> {
    print("Running operation 3")
    if Bool.random() {
        return .failure(.operation3Failed)
    } else {
        return .success(String(input.map { _ in "👍🏼" }))
    }
}

func run() -> Result<String, CustomErrors> {

    let r = operation1()
        .flatMap { operation2($0) }
        .flatMap { operation3($0) }

    return r

}

print(run())
print("-------------------------")
print(run())
print("-------------------------")
print(run())

By running these operations, we can use the output of an operation into another. Here we've added some random failures so we can see how it works.

This episode uses Xcode 11.2.1, Swift 5.1.

Want more? Subscribers can view all 583 episodes. New episodes are released regularly.

Subscribe to get access →

Source Code

View on GitHub Download Source