I want to share a technique that I have found useful when using the Swift DO, try, catch error handling model to limit the number of errors thrown from a given API call.

Currently, Swift does not provide type errors (called “checked exceptions” in other languages, such as Java), which means that any function thrown can throw any error. While this gave us a lot of flexibility, it was also a challenge for production code and testing when using the API.

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

class AClass {
    enum SearchError: Error {
        case invalidParameters(String)
    }
    
    func loadData(_ parameters: String) throws -> Data {
        let urlString = "https://host/q=\(parameters)"
        
        guard let url = URL(string: urlString) else {
            throw SearchError.invalidParameters(parameters)
        }
        
        return try Data(contentsOf: url)
}
Copy the code

As you can see above, our function can end up in two different places (when we try to construct a URL and when we initialize Data with a URL). So, that’s the problem; As an API user, I have no idea what kind of errors I can expect this function to throw. I don’t need to know what types this function uses inside Data, but I also need to know what errors the Data initializer can throw.

Having to know the imlpementation details is usually a bad sign in API design, so wouldn’t it be nice if we could make sure that our function only throws errors of type SearchError? Fortunately, it’s easy to fix. All we need to do is wrap the call to the data in a do, try, catch block. The refactoring code looks like this:

class AClass {
    enum SearchError: Error {
        case invalidParameters(String)
        case loadingFailed(URL)
    }
    
    func loadData(_ parameters: String) throws -> Data {
        let urlString = "https://host/q=\(parameters)"
        
        guard let url = URL(string: urlString) else {
            throw SearchError.invalidParameters(parameters)
        }
        
        do {
            return try Data(contentsOf: url)
        } catch {
            throw SearchError.loadingFailed(url)
        }
    }
}
Copy the code

What we did above is to replace the exception thrown by initializing Data with our own error. Now we can record that our function always throws a SearchError, and our API has become much easier to use in error handling.

### However, while making our API better, our implementation became messy. Often, you need to wrap multiple calls with do, try, catch blocks, which can quickly make our code unreadable. To solve this problem, I created a simple function that performs this wrapper and throws specific errors when the underlying errors are thrown. It looks something like this:

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

Using it, we can now update our loadData function from scratch to make it simpler:

func loadData(_ parameters: String) throws -> Data {
        let urlString = "https://host/q=\(parameters)"
        
        guard let url = URL(string: urlString) else {
            throw SearchError.invalidParameters(parameters)
        }
        
        return try perform(Data(contentsOf: url), orThrow: SearchError.loadingFailed(url))
    }
Copy the code

We now have a unified error API and a simpler implementation!

This approach to problems is also present in Alamofire, such as the encode function in JSONEncoding

public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = try urlRequest.asURLRequest()

        guard let parameters = parameters else { return urlRequest }

        do {
            let data = try JSONSerialization.data(withJSONObject: parameters, options: options)

            if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
                urlRequest.setValue("application/json".forHTTPHeaderField: "Content-Type")
            }

            urlRequest.httpBody = data
        } catch {
            throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
        }

        return urlRequest
    }
Copy the code

Requestable is also present in DataRequest:

func task(session: URLSession, adapter: RequestAdapter? , queue: DispatchQueue) throws -> URLSessionTask {do {
                let urlRequest = try self.urlRequest.adapt(using: adapter)
                return queue.sync { session.dataTask(with: urlRequest) }
            } catch {
                throw AdaptError(error: error)
            }
        }
Copy the code

If you have any questions, suggestions or feedback, please feel free to contact me ~