Understanding the Result Type in Swift

Error handling when expecting a result out of an operation is a very common thing to do. For this reason, various high-level programming languages have introduced a Result type into their libraries, on top of their existing error-handling features. This feature was implemented in Swift 5.

A Result wraps a success or a failure. It is essentially an enum with two possible cases: .success and .failure. The .success case wraps the correct result of an operation, whereas a .failure wraps an Error. Its implementation uses generics, so you always know what you are going to get back.

In this article, we will explore why you would want to use the Result when we can already handle errors with do-try-catch and specifying functions that throw an error with throws.

Using Result

To understand Result better, we will explore all the ways you could want to write a function to read the contents of a URL into a String. On each step, we will see the shortcomings of the traditional do-try-catch APIs and the advantages of Result. This is a good example to build, because it’s a task that is likely to throw an error, and there’s different reasons that may trigger it.

The Traditional Error Handling Approach.

We will start by creating our own error object to handle all file reading operation errors:

enum StringFileError: Error {
    case invalidURL
}

Now, the first attempt to write a file-reading function that can throw errors could look like this:

func readFileFromURL(url: URL?) throws -> String {
    guard let url = url else { throw StringFileError.invalidURL }
    return try String(contentsOf: url)
}

Because the function is marked as throws, we have to handle the error ourselves.

do {
    let fileContents = try readFileFromURL(url: URL(string: "/path/to/file"))
} catch let error as StringFileError {
    // Handle StringFileErrors
} catch {
    // Handle any other errors thrown by the SDK.
}

When trying to read a file from a URL, we can receive StringFileErrors or we can receive Errors, depending on what the problem is.

That code gets the job done, but we can improve it with the new Result type.

Implementing Result Over Traditional do-try-catch

To start using result, we can change the signature of the function and its implementation to return a new Result<String, Error> instead. It’s still far from what we want to achieve, but we will get there in a second.

func readFileFromURL(url: URL?) -> Result<String, Error> {
    return Result<String, Error> {
        guard let url = url else { throw StringFileError.invalidURL }
        return try String(contentsOf: url)
    }
}

To use it, we need to change a few things and even add an extra line of code:

do {
    let fileContentsResult = readFileFromURL(url: URL(string: "/path/to/file"))
    let fileContents = try fileContentsResult.get()
} catch let error as StringFileError {
    // Handle StringFileErrors
} catch {
    // Handle any other errors thrown by the SDK.
}

This is not really a good example of when you’d want to use result. The most important feature of Result to me is that you can start using type-safe errors. One of the biggest weaknesses in Swift right now is that errors are not type-safe. Despite how strong-typed Swift is, there is no way to tell if a function can throw specific errors (like StringFileError) - everything just throws and whatever it throws can be an Error of any type, whether it is the high-level Error itself or specific implementations.

If you knew that all your file paths are always going to point to a valid file, for example, you can specify the error in result to be of StringFileError, and we gain some additional safety.

We can also rewrite the implementation of function, while still using result. Sometimes, initint a result type with the closure can be a bit tricky, so we can instead make our function return .success or .failure.

func readFileFromURL(url: URL?) -> Result<String, StringFileError> {
    guard let url = url else { return .failure(.invalidURL) }
    return .success(try! String(contentsOf: url))
}

To handle the error, we now have just one case to consider, so we can get rid of the last catch clause.

do {
    let fileContentsResult = readFileFromURL(url: URL(string: "/path/to/file"))
    let fileContents = try fileContentsResult.get()
} catch {
    // Now all the possible errors are StringFileErrors, so we can simplify the catch clauses into one
}

And you can make it even prettier:

let fileContentsResult = readFileFromURL(url: URL(string: "/path/to/file"))

switch fileContentsResult {
    case .success(let contents): print(contents)
    case .failure(let stringFileError): // Handle error
}

Conclusion

One of Swift’s weaknesses is that it can’t deal with errors in a type safe manner despite how strongly-typed it is. The Result type helps us write code where errors are typed, and this can help us write cleaner code.