Celebrating 10 Years!

profile picture

Swift Enums for Error Reporting

January 09, 2016 - Roundwall Software

In Apple's block-based API's, you'll often see a pattern like this:

func dataTaskWithRequest(request: NSURLRequest, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask

The function expects you to provide an NSURLRequest and a block and promises to execute your block later with some info. Inside your block you might get some data, you might get a response, and you also might get an error. Optionals help to make this more clear than it was in Objective-C by forcing you to consider that each parameter might be nil. To handle all the possible combinations, you might have to do something like this:

if data == nil {
  if error != nil {
    print("Didn't get any data, but instead got this error that might explain why: \(error)")
  }
}

if let data = data, let response = response {
  print("The server responded with these headers and code \(response) and this data: \(data)")
}

This involves learning which combination of things you can expect and handling each case. What if we could make our API's so that they're more clear and gave us less opportunity to mess up when we're tired or distracted?

What if we built an enum with cases like this:

enum RequestResult {
  case NoConnection,
  case Success(data: NSData)
  case HTTPError(error: ErrorType)
}

We could then wrap NSURLSession's method into one more like this:

func dataTaskWithRequest(request: NSURLRequest, completionHandler: (RequestResult) -> Void) -> NSURLSessionDataTask

Then when we use it later, we're forced to handle all the possible cases without needing to remember what they are.

switch result {
  case .NoConnection:
    print("No connectivity, can't try to connect to server")
  case .Success(let data)
    print("Request worked just fine, here's the headers and such \(response) and the data: \(data)")
  case .HTTPError(let error)
    print("Did connect, but server responded with a not-happy HTTP status code: \(code), error: \(error)
}

Then if we forgot a possibility, the compiler could say "Hey woah, you have a switch and you didn't consider one of the possible cases!" so we wouldn't need to rely on our mental state to keep up. For each possible outcome, we also made it clear which objects we can expect. No optionals, no matrix of possibilities, just a clear set of variables we can expect based on the outcome. If it's considered a successful request, we can expect the data we asked for. Future developers (including yourself 6 months from now) will have a much easier time handling the response of these HTTP requests without forgetting a step or missing an important variable. Hooray!

Once this is working and you're feeling especially fancy, you could even take this further. Maybe you make ConnectionResult into some kind of generic thing that doesn't just hold data on a successful request, but instead could also hold model objects constructed from the JSON your server gave you. Maybe something like Result<T, ErrorType> which is an enum that includes either a success case with an object of type T or a failure case with error information. Maybe then you could add some function to your Result type that lets you feed your result a function that should only be executed if your result is a success case. Something like:

func map<T>(transform: (T -> U)) -> Result<U, ErrorType>)

I'm just calling it map because I though that might be a good name. This would allow you to turn your result of type Result<T, NSError> to one of type Result<U, ErrorType>, like if T were an NSDictionary and the U was your FeedItem model object. If your Result were a success, you'd get back a new success that contained your FeedItem, but if your original result were instead an error, you'd get back a new failure case with the same error. This lets you do nifty things like this:

func dataTaskWithRequest(request: NSURLRequest, completionHandler: (result) -> Void) -> NSURLSessionDataTask {
  let dataModel = result.map(processModel)
  print("Got a model back from the server: \(dataModel)")
}

Where processModel is a function that doesn't know or care about Result. It just knows how to turn NSDictionaries into data models. This convenience function lets us avoid writing switch statements everywhere, and still preserve the context we've built up that our value can either be successful with a useful object or a failure with some error information about why we couldn't do it. Better living through abstractions!

Congratulations, you just made a Functor. You've practically created the Voldemort of Swift development (the Monad, cough cough).