URLSession.shared.dataTask and HTTP errors such as 404

The URLSession.shared.dataTask completionHandler passes us three pieces of information. Data, the response from an HTTP request and an error.

Transport errors are returned within the error object but HTTP status codes which we’d possible class as errors are supplied in the response object, so I wanted both transport and HTTP status errors to be returned as an Error.

Note: I’m not using URLSession.shared.data as it’s not currently supported on Linux, from what I can tell.

The code below creates a new dataTask method that is async and returns a Result<Data?, Error>. Let’s first look at new dataTask method without the status code handling

private func dataTask(with request: URLRequest) async -> Result<Data?, Error> {
   await withCheckedContinuation { continuation in
      URLSession.shared.dataTask(with: request) { data, response, error in
         if let error = error {
            continuation.resume(returning: .failure(error))
            return
         }
         continuation.resume(returning: .success(data))
    }.resume()
}

So this creates a continuation which is used as a promise/future/task by calling the continuation resume method with either a success or failure.

As already mentioned, this will not return 404’s (for example) as errors. So let’s simply add code to create a failure when certain HTTP status codes appear. We might want to differentiate between the two types of error, i.e. transport layer and HTTP, so first off let’s create our Error subclass, which looks like this

public enum HTTPError: Error {
    case transportError(Error)
    case httpError(Int)
}

This will allow us to pass back the Error from the URLSession.shared.dataTask completion handler for transport errors and the code from the HTTP status for HTTP errors.

Now we’ll change the earlier code to look like this

private func dataTask(with request: URLRequest) async -> Result<Data?, Error> {
   await withCheckedContinuation { continuation in
      URLSession.shared.dataTask(with: request) { data, response, error in
         if let error = error {
            continuation.resume(returning: .failure(HTTPError.transportError(error)))
            return
         }

         let resp = response as! HTTPURLResponse
         let status = resp.statusCode
         guard (200...299).contains(status) else {
            continuation.resume(returning: .failure(HTTPError.httpError(status)))
            return
         }

         continuation.resume(returning: .success(data))
    }.resume()
}

Now we can check the Result<Data?, Error> for the error and see what type of error it is, for the example below I actually throw the error for the caller to catch

let result = await dataTask(url: url!, httpMethod: "POST", httpBody: httpBody)
if case .failure(let error) = result  {
   throw error            
}