BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Previewing Swift 5 Result Type

Previewing Swift 5 Result Type

Leia em Português

Bookmarks

One of the most awaited proposals for Swift 5, Result, has already landed into the language. The Result type forces the programmer to explicitly handle the failure and success cases before they can gain access to the actual value. Let’s have a look at how it is implemented, how you can use it, and why it was needed.

The Result type is meant to enforce a safe way to handle errors returned by a function call without resorting to exceptions. While exceptions provide an automatic mechanism for error propagation and handling, Result provides a manual mechanism with stronger type guarantees and more suitable to async error handling. Similarly to the Option type, the Result type is a monad.

The Result type in Swift 5 is implemented as an enum with two cases: .success and .failure.

public enum Result<Success, Failure: Error> {
  /// A success, storing a `Success` value.
  case success(Success)

   /// A failure, storing a `Failure` value.
  case failure(Failure)
  ...
}

You can define a simple function returning a Result and handle its result like this:


enum AnErrorType: Error {
    case failureReason1
    case failureReason2
}

func failableFunction() -> Result<Int, AnErrorType> {

    ...
    if (errorCondition1) {
        return .failure(.failureReason1)
    }
    ...
    return .success(1)
}

func callFailableFunction() {

    ...
    let result = failableFunction()
    switch result {
    case .success(let integerResult):
        ...
    case .failure(let error):
        switch error {
        case .failureReason1:
            ...
        case .failureReason2:
            ...
        }
    }
    ...
}

Additionally, to make it easier to integrate with existing functions, the Result type supports a specific initializer that accepts a closure that may throw:

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

Being a monad, Result implements the known map() and flatMap() methods (along with mapError() and flatMapError() which are conceptually the same):

  • map() (mapError()) enables the automatic transformation of a value (error) through a closure, but only in case of success (failure), otherwise the Result is left unmodified.

  • flatMap() (flatMapError()) is useful when you want to transform your value (error) using a closure that returns itself a Result to handle the case when the transformation fails. In such cases, map() (mapError()) would return a <Result<Result<...>>. With flatMap() (flatMapError()), you get a simple, flattened Result instead.

Result types represent an improvement over the do try catch throw syntax which is the basic mechanism for error handling in Swift since Swift 2 for a number of reasons.

First and foremost, using Result types it becomes so much more natural to handle async failures. A typical pattern to handle async function calls in Swift makes use of callbacks, like in the following example:

asyncOperationCall() { (value, error) in

    guard error != nil else { self.handleError(error!) }
    self.handleValue(value)
}

As you may easily recognize, using callbacks defeats the purpose of exceptions in Swift, which is automatic propagation and handling of errors. Instead, with async callbacks, error must be handled in place, because when an exception may be thrown from a callback it is already too late for the error to be automatically propagated up the call stack. On the contrary, a Result may be stored and processed at a later point when someone else attempts to use the returned value. This property, called delayed error handling, is not specific to async code and may greatly simplify Swift syntax, as shown in the following example:

do {
    handleOne(try String(contentsOfFile: oneFile))
} catch {
    handleOneError(error)
}

do {
    handleTwo(try String(contentsOfFile: twoFile))
} catch {
    handleTwoError(error)
}

do {
    handleThree(try String(contentsOfFile: threeFile))
} catch {
    handleThreeError(error)
}

The code above translates into the following much more readable snippet in Swift 5:

let one = Result { try String(contentsOfFile: oneFile) }
let two = Result { try String(contentsOfFile: twoFile) }
let three = Result { try String(contentsOfFile: threeFile) }

handleOne(one)
handleTwo(two)
handleThree(three)

Another advantage of Result is that we know for sure we either get an error or a value. When using a callback, we have actually four different combinations that we should think about: a good value with no error; no value with an error; a value with an error; no value and no error.

Finally, using a Result type enables constraining the exact error type that may be returned. In the first example we gave above, this property allowed us to fully enumerate the possible error causes, i.e., .failureReason1 and .failureReason2, along the error handling path. On the contrary, a Swift function that can throw does not specify the type that can be thrown, so when handling the error condition in a catch block, you only know your error conforms to the Error protocol.

Swift 5 is planned to be released early 2019 and focuses on bringing ABI stability to the language.

Rate this Article

Adoption
Style

BT