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<T>(_ 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.

If you have questions, feedback or comments — feel free to either post a reply here or contact me on Twitter @johnsundell.

Thanks for reading 🚀

I write weekly blog post about Swift development here on Medium, and also work on several Swift open source projects on GitHub. You can also follow me on Twitter for updates on all my Swift adventures.