Providing a unified Swift error API

I want to share a technique that I’ve come to find quite useful when using the Swift do, try, catch error handling model, to limit the amount of errors that can be thrown from a given API call.

At the moment, Swift does not provide typed errors (what is known as “checked exceptions” in other languages, like Java), which means that any function that throws, can potentially throw any error. While this gives us a lot of flexibility, it can also be a bit of a challenge when it comes to using the API, both for production code and in testing.

Consider the following function, which performs a search by loading data synchronously from a URL:

func loadSearchData(matching query: String) throws -> Data {
    let urlString = "https://my.api.com/search?q=\(query)"

    guard let url = URL(string: urlString) else {
        throw SearchError.invalidQuery(query)
    }

    return try Data(contentsOf: url)
}

As you can see above, our function can throw in two different places — when attempting to construct a URL and when initializing Data with the URL. So, here’s the problem; as an API user, it becomes very unclear what kind of errors I can expect this function to throw. Not ony do I need to be aware that this function uses the Data type internally, but I also need to know what errors that Data’s initializer can throw.

Having to be aware of imlpementation details is usually a bad sign when it comes to API design, so wouldn’t it be better if we could guarantee that our function only throws Errors of the SearchError type? Luckily, it’s easily fixed. All we have to do is wrap the call to Data in a do, try, catch block. Like this:

func loadSearchData(matching query: String) throws -> Data {
    let urlString = "https://my.api.com/search?q=\(query)"

    guard let url = URL(string: urlString) else {
        throw SearchError.invalidQuery(query)
    }

    do {
        return try Data(contentsOf: url)
    } catch {
        throw SearchError.dataLoadingFailed(url)
    }
}

What we do above, is to silence the error thrown by Data, and replace it with our own error instead. Now, we can document that our function always throws a SearchError, and our API becomes a lot easier to use when it comes to error handling.

However, in making our API better, we’ve also cluttered up our implementation a bit. Often you’ll need to wrap multiple calls with do, try, catch blocks, which will make our code quickly become harder to read. To solve this problem, I’ve made a simple function that does this wrapping, and throws a specific error in case an underlying error was thrown. It looks like this:

func perform(_ expression: @autoclosure () throws -> T,
                orThrow error: Error) throws -> T {
    do {
        return try expression()
    } catch {
        throw error
    }
}

What perform does, is to execute a throwing expression and throw a custom error in case it failed. Using it, we can now update our search function from before, to make it a lot simpler:

func loadSearchData(matching query: String) throws -> Data {
    let urlString = "https://my.api.com/search?q=\(query)"

    guard let url = URL(string: urlString) else {
        throw SearchError.invalidQuery(query)
    }

    return try perform(Data(contentsOf: url),
                       orThrow: SearchError.dataLoadingFailed(url))
}

We now have both a unified error API, and a simpler implementation! 🎉

Feel free to use the perform function in your projects — I put it up on GitHub as a Gist here.

Feel free to reach out to me on Twitter if you have any questions, suggestions or feedback. I’d also love to hear from you if you have any topic that you’d like me to cover in an upcoming post.

Thanks for reading 🚀

Type erasure using closures in Swift