Exhaustivity test when dealing with optional errors

In my http request code, I'm switching over the (data, response, error) tuple passed to the completion handler in dataTask(with:completionHandler:) and turning it into a promise, like so:

func dataTask(with request: URLRequest) -> Promise<Data?, RequestError> {
    return Promise { done in
        let task = self.urlSession.dataTask(with: request) { data, response, error in
            let status = HTTPStatus(form: response)
            // Error: Switch must be exhaustive
            switch (error, status, data) {
            case let (urlError as URLError, _, _):
                done(.failure(.networkError(urlError.code)))
            case let (error as NSError, _, _):
                done(.failure(.other(error)))
            case let (_, _, data?) where data.containsGeoblockError():
                done(.failure(.geoblocked))
            case let (_, status?, data) where status.isFailure:
                done(.failure(.httpError(status, data: data)))
            // Warning: Case is already handled by previous patterns; consider removing it
            case let (_, _, data):
                done(.success(data))
            }
        }
        task.resume()
    }
}

I've been able to reduce the example to this:

let error: Error?

switch error {
case let error as NSError: 
    // error should be non-nil and bridged to NSError
case .none:
    // error should be nil
}

The exhaustivity checker seems to think that the let error as NSError line will catch every case. But it doesn't catch the nil-case. I can trivially fix it like so:

let error: Error?

switch error {
case .some(let error as NSError): 
    // error is non-nil and bridged to NSError
case .none:
    // error is nil
}

Or, in my original code:

switch (error, status, data) {
case let (urlError as URLError, _, _):
    done(.failure(.networkError(urlError.code)))
case let (.some(error as NSError), _, _):
    done(.failure(.other(error)))
case let (_, _, data?) where data.containsGeoblockError():
    done(.failure(.geoblocked))
case let (_, status?, data) where status.isFailure:
    done(.failure(.httpError(status, data: data)))
case let (_, _, data):
    done(.success(data))
}

However, it should work, non? Eg. the URLError casting only works for non-nil values. Should this be reported as a bug?

(Tangential: Can URSession give some guarantees as to what kinds of errors that it will give. What non-URLErrors can I expect to get? Maybe I should ask that in the Apple Forums?)

Can URLSession give some guarantees as to what kinds of errors that
it will give.

No. In practice it almost always gives you errors from the NSURLErrorDomain domain, but there’s no guarantee of that.

What non-URLError [values] can I expect to get?

I can’t think of any off the top of my head.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

Thanks. Regarding the main topic of the thread. Is it a bug? Should I report it as such?

POSIXError can also be produced, at least. We started seeing error code 53 on iOS 13.

1 Like

Regarding the main topic of the thread.

Sorry, I don’t have an opinion on that, other than my general take on such things, namely, if it annoys you, file a bug. Even if the behaviour is working as designed, if it’s sufficiently annoying then the design should change.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

I would typically unwrap the error first. No need to switch over it if it's nil.

if let error = error {
// handle the error as you like
} else {
// success!
} ```

If the compiler thinks that you can exhaustively switch over an Error? with case x as NSError, that's definitely a compiler bug.

1 Like