Brad Larson of Sunset Lake Software wrote a blog post called “Why we’re rewriting out robotics software in Swift.” The article is a worthwhile read, and explains that an audit of their Objective-C code history revealed that approximately 40% of the shipped bugs would have been prevented by using Swift. Which is really interesting.
I would like to focus on a different part of the article, however. It’s actually more of an aside in the article itself, but it was something that really stood out to me: the use of enums instead of NSError in Swift. Brad goes into more detail on the subject in a mailing list post. After having played with it myself, I think using enums for errors is a really big win, and I intend to use the pattern in my own Swift code going forward.
What’s Painful About NSError
NSError is used in code built on the Foundation framework for recoverable error situations. By merely receiving an NSError object, you know something went wrong, and you can inspect the object to learn more detail about what happened. A good writeup on NSError can be found at NSHipster.
Unfortunately, constructing and inspecting NSErrors can be cumbersome. To create an NSError, you need to specify a domain and a code. The domain should be a reverse DNS-style string, like “com.company.app”. The code needs to be an integer that’s unique within the error domain. The domain and code tend to be of limited usefulness when using NSErrors later, but you need to supply them when creating NSError objects anyway.
The more-often useful information in an NSError lives in its userInfo dictionary. You provide values for a few system-defined keys like NSLocalizedDescriptionKey, and you can attach any additional objects you wish to the same dictionary. I often need to refer to documentation to remember NSLocalizedDescriptionKey, and attaching extra information to the dictionary requires you to keep track of the keys you use in order to consume them later. Picking apart an NSError very frequently involves trips to a header file or framework documentation.
Using Swift enums nicely addresses these complaints.
A Swifty Enum Error
So what does an enum error look like? Here’s an example of a hypothetical error type for interacting with an API server.
A function which can produce an error could look like this.
Creating errors becomes easy, clear, and descriptive. It’s even statically-typed, so the compiler will require you to construct the error correctly.
Consuming errors is simple, as well. You could use a switch statement to handle some—or all—cases.
Often you just want a textual description of the error. You might want to put the text into an alert view, or you might want to log out the error description while debugging. This can be handled by having your error type implement the Printable protocol.
Printing the error then becomes as simple as this.
In my time with this style of error, I believe they are far easier to construct and consume than NSError, so I quite like them.
What About Objective-C Compatibility?
Unfortunately, Swift enums can’t export to Objective-C. So what if you want to use these awesome enum errors in Swift, but still need your code to be consumable from Objective-C? In that case, you need to provide a way to translate the enum error to an NSError object, and then export the NSError object to Objective-C.
You can pull this off by adding a foundationError property to your enum error.
Then, in code that can return an error, you must provide overloads that return an NSError.
The NSError-returning overload will be the only version made available to Objective-C, so it’ll be as if your enum error code doesn’t even exist there.
Since both overloads will be visible from Swift, however, you’ll need to be explicit about the overload you call, instead of letting Swift infer the type.
It’s not the best solution, since you have to add extra code to handle codes and domains to the error enum, and you have to use explicit typing when calling from Swift, but it still more or less gets you the best of both worlds.
Of course, the better long-term solution to this problem is to stop using Objective-C altogether and just use Swift full-time :D