Nils vs Throws in #swiftlang Part II

Yesterday, I posted about when to use nil and when to use throw when working with Swift 2.0. I suggested avoiding nil as a semaphore, that is don’t use nil to signal an error state. Swift 2.0’s new system includes simple ErrorType enumerations and do/try/catch.

These enable you to better represent distinct error conditions compared to naked nils and to handle situations where you’d normally leave a scope and report an error. Error handling simplifies return types to avoid optionals and unwrapping in failure situations.

I warn you I got very little sleep last night so the following info will probably be incoherent.

This isn’t to say that nils and optionals don’t remain important in Swift 2.0. When you look up a key in a dictionary and it’s not found, it’s usually not an error. It’s just a nil result. If your code would not otherwise look like “if nil print an error and then leave scope”, you probably don’t have to replace your if-let syntax with try/throws.

Importantly, what try/throws gets you is error reporting strictly at the point where the error occurs. A method that looks like

func someThrowingMethod() throws -> resultType {
    try someCall()
    // ... do other things ...

participates in the error handling system but does not itself create or consume errors. Any issues that pop up during someCall get passed down the error handling chain until they’re caught.

Here’s a comparison between the two approaches. This is the “traditional” approach for values of traditional approaching 1.2:

And here’s the new style approach:

These examples share the same overall shape. A consumer function  works through a series of call to something that might fail.

In the old style approach, you have to ask, “who’s responsible for reporting and handling errors?” It’s unclear and it’s completely possible for every single function along the way to report errors (as you see here) or none of them. The errors have no unifying structure. They are just ad-hoc print statements. Plus, every single function has to deal with testing and unwrapping intermediate values. They pass nothing along to the calling functions other than “I did not succeed”.

That responsibility question is answered by default in the new system. The do-catch system provides a natural end-point for throws. Once an error is handled, it’s handled. Mid-chain items know they’re working in a failable system, evidenced by try and throws. They do not, however, take any responsibility for error handling. Errors are only mentioned at the point where they’re created and the point where they’re consumed. And at no point do you have to reach into a container to pull out a value.

You can update the “traditional” system to use errors as well as optionals but I honestly don’t see an advantage here. You probably also have to add a No-Error state to your Swift error enumeration because an error var needs to be initialized before use. Or just use NSError/NSErrorPointer instead. And pretty soon you’re just back at the current system except with nils instead of try.

Several readers asked yesterday about optional chaining and nil vs throw. Chaining ideally creates a parsimonious and readable set of operations that flow smoothly from one call to the next. Using optionals enables that chain to break at a point of failure. Would I recommend redesigning these calls? If you’re not checking error conditions already, I don’t really see why you would redesign. And if you already are, I doubt you’d be using optional chaining there to begin with.

And what about async with completion handling? I aesthetically prefer providing both an error handler and a completion handler over completion(maybeData, maybeError) but so long as there’s one catch somewhere along the line on secondary threads, it doesn’t really much matter who’s responsible.

Re-architecting code is a pain. Moving libraries to the new error handling system is no exception. I think there’s sufficient win with the new system to make it worth your time.

Comments are closed.