Matching NSError in a catch

EXCITING UPDATE!

Swift is way more on top of this than I originally gave it credit for. The TL;DR is you can actually match NSErrors using their structs like so:

catch URLError.timedOut {
  print("timed out!")
}

The updated post over here has all the details.

For context, the rest of the original is provided blow. But seriously, check out the update.


Say we’re writing some sort of networking library on top of URLSession. We might define some domain-specific errors that only make sense in the context of our library — unexpected headers or things like that:

enum RequestError: Error {
  case missingContentType
  case missingBody
  //...
}

And these would be really easy for a client using our library to catch:

let net = AmazingNetworkThing()
do {
  try net.fetchRequest(myRequest)
} catch RequestError.missingContentType {
  print("unknown content type")
} catch {
  //...
}

But the vast majority of network-related errors would actually be raised by the URL loading system itself. And that’s Foundation-level API, which means it raises NSErrors. Which, in turn, means a consumer of our library would have to catch a simple timeout something like:

do {
  try net.fetchRequest(myRequest)
} catch let error as NSError where
     error.domain == NSURLErrorDomain &&
     error.code == NSURLErrorTimedOut {
  print("timed out!")
}

By comparison, this feels overly wordy and very imperative.

We could, of course, catch every NSError thrown by URLSession in our library, and wrap it in a custom enum-based error for easier matching by the client. But a few problems with this approach:

  1. That’s a lot of boilerplate. NSURLErrorDomain defines constants for 49 error codes. And there could be more that are undocumented. Which brings us to…
  2. We don’t control URLSession. By abstracting over part of its API in our library, we’re putting ourselves on the hook for keeping pace with Apple devs from point-release to point-release. And they have a bigger team than ours. Which also suggests…
  3. URLSession is much more widely adopted than our library. Consumers of our lib way down the stack might be expecting a NSURLErrorDomain error rather than our custom, one-off wrapper.

So rather than wrapping the error, a better solution is to wrap the matcher.

struct ErrorMatcher {
  let domain: String
  let code: Int
}
 
func ~=(p: ErrorMatcher, v: Error) -> Bool {
  return 
     p.domain == (v as NSError).domain &&
     p.code == (v as NSError).code
}

See this post for more on that funky ~= operator.

Using this, we can more elegantly and declaratively match any NSError:

do {
  try net.fetchRequest(myRequest)
} catch ErrorMatcher(
     domain: NSURLErrorDomain,
     code: NSURLErrorTimedOut) {
  print("timed out!")
}

And if we were to add a little sugar for a domain of particular concern to us, well who would blame us?

extension ErrorMatcher {
  static func urlDomain(_ c: Int) -> ErrorMatcher {
    return ErrorMatcher(
       domain: NSURLErrorDomain, 
       code: c)
}
 
// Then...
do {
  try net.fetchRequest(myRequest)
} catch ErrorMatcher.urlDomain(NSURLErrorTimedOut){
  print("timed out!")
}

And if a given domain/code pair were common enough, we could even:

extension ErrorMatcher {  
  static let urlTimeout = 
     ErrorMatcher.urlDomain(NSURLErrorTimedOut)
}

// Thus...
do {
  try net.fetchRequest(myRequest)
} catch ErrorMatcher.urlTimeout {
  print("timed out!")
}

Thankfully we don’t have to toss out all these existing, localized, very well documented NSError babies with the bath water of imperative matching. Leveraging Swift’s expression patterns, we can have declarative enum-like matching in our catch clauses — even with NSErrors.