Thursday, February 12, 2015

I Ain't Afraid of No NSError: Adventures in API Design

These are the notes from my Lightning Talk at Xcoders Seattle on February 12, 2015. A Lightning Talk is a 5-10 minute presentation that gets to the heart of an idea, problem, solution, quibble, etc. They are popular at conferences (also called Blitz Talks) where multiple will be held over 60-90 minutes.

The local Xcoders has begun running this as a way to let those without a 60+ minute presentation bring new ideas into the local developer community. I think it's great and if your developer meetup is not using them, then give them a try.

--
Adventures in API Design

My Point

NSError's userInfo dictionary is the right way to return errors from your API. Let me elaborate.

Designing an API

Everything is a trade off. When designing your API there are multiple things to consider:
  • How much of the internal API should I expose?
  • Should I provide logging?
  • How long is too long for a method name?
  • How should I return errors?
Let's focus on errors. Those are easy, right?


Throw an Exception?

We have several options available and if you are coming to Cocoa (Objective-C) programming from other languages then you might think using NSException to throw exceptions is an option, right? 

Wrong!

Apple has made it clear through it's use of NSError that exception throwing is not the answer. Exceptions are things you should fix/not handle in code and continue on.

With Swift they have now codified this desire. Swift has done away with the ability to catch exceptions.

Ways of Using NSError

Ok, so exceptions are definitely not the way. So how can we pass errors back to our users? And how can we do that if we want to provide more info than NSError provides?

A quick review of some popular 3rd Party APIs shows that we have several options for error handling:
  1. Subclass NSError.
  2. Use Categories to extend NSError.
  3. Use NSError and it's userInfo NSDictionary.

Option 1: Subclassing

This Sounds like a great idea if you come from other OO languages. But with Objective-C we run into a corners that can catch us up.  Here are some problems:
  • Friction. 
    • Devs need to handle NSError and YourNSError.
  • Crashes
    • Crashes make 3rd party dev mad!
    • Why crashes? If you have an API call that says it returns YourNSError but you return NSError thinking no problem (or by mistake) then what happens the the 3rd Party Dev calls one of your new methods that you added to YourNSError? Their app will crash since the selector (method) is not available on the NSError you actually returned.
  • Slowed code
    • After a crash due the scenario listed above the 3rd Party Dev must use reflection using NSObject's respondsToSelector: to verify that your returned error objects contain the listed methods. Reflection is slow, why add the overhead to a 3rd Party Devs App?

Option 2: Add a Category

Ok, so subclassing is perhaps not the answer but what about just adding a Category to NSError and adding your new properties and methods to this category?

First, if you do not know that you can add properties to an existing class using a category, then look up Associated Objects. I don't recommend you try this. You warp the very foundations of the classes you are using and cause more harm than good in most cases.

But, given that some popular APIs use just this method. What are the pros to extending NSError like this?
  • Less Up-Front Friction for Devs
    • No messy switching between a subclass and NSError.
What are the cons?
  • Crashes
    • Ok, so you decided to add some new properties using the magic of the Objective-C runtime. You learned about Associated Objects ability to extend a class with a property and found a "back door" around subclassing.
    • The thing about adding a property through object association is that if the API does not set the property then it has not been associated with the object. Again, we have a missing selector and we have caused a crash.
    • Dev no like crash! 
  • Slower code
    • We are back to having to use reflection (respondsToSelector:) to verify your error objects support the properties you listed. Boo!!
Option 3: It's So Not OO

A common complaint I hear about Apple APIs is the reliance on NSDictionary. NSError is one of those culprits with it's userInfo. It does not feel OO so many poo-poo it as an aberration.

So what? It's how it's done and it has good reason. So why use a pure NSError and pass back additional details in userInfo?
  • Less Friction
    • No learning a new NSError class/category.
  • No Reflection = Faster Code
    • No reflection needed to validate your error objects support your listed properties or methods.
  • Reduced Crash Vector
    • If the user makes a call to [userInfo objectForKey:] on a non-existing key, then instead of a crash they will receive nil which in Objective-C is a no-op (meaning no crash). This is unlike a call to a non-existing property/method (when misusing a Category or subclass) which will crash with an NSInvalidArgumentException with "unrecognized selector sent to instance".
  • For the high runner case you don't need more than NSErrors (domain, code, and localizedDescription).
  • For the low runner cases you can extend NSError with additional details in the userInfo dictionary by adding new keys, publishing them in your API docs, and then let the app developer decide if they want that added detail.
  • If the dev doesn't really care, they can just print the entire userInfo and get all the details of your error.

Avoid friction.
Don't slow down code.
Avoid skip crashes. 

The right way to expose your API errors is with NSError.

References


For further reading, start here:

No comments: