For around a month at the end of last year, I kept a long text note called “doing doing it wrong right”. I was trying to understand error handling in programming, look at some common designs and work out a plan for cleaning up some error-handling code I was working with myself (mercifully someone else, with less analysis paralysis, has taken on that task now).
Deliciously the canonical writing in this field is by an author with the completely apt name Goodenough. His Structured Exception Handling and Exception Handling: Issues and a Proposed Notation describe the problem pretty completely and introduce the idea of exceptions that can be thrown on an interesting condition and caught at the appropriate level of abstraction in the caller.
As an aside, his articles show that exception handling can be used for general control flow. Your long-running download task can throw the “I’m 5% complete now” exception, which is caught to update the UI before asking the download to continue. Programming taste moved away from doing that.
In the Cocoa world, exceptions have never been in favour, probably because they’re too succinct. In their place, multi-if statement complex handling code is introduced:
NSError *error = nil;
id thing = [anObject giveMeAThing:&error];
if (!thing) {
[self handleError:error];
return;
}
id otherThing = [thing doYourThing:&error];
if (!otherThing) {
[self handleError:error];
return;
}
id anotherThing = [otherThing someSortOfThing:&error];
…and so it goes.
Yesterday in his NSMeetup talk on Swift, Saul Mora reminded me of the nil
sink pattern in Objective-C. Removing all the error-handling from the above, a fluent (give or take the names) interface would look like this:
id anotherThing = [[[anObject giveMeAThing] doYourThing] someSortOfThing];
The first method in that chain to fail would return nil
, which due to the message-sink behaviour means that everything subsequent to it preserves the nil
and that’s what you get out. Saul had built an equivalent thing with option types, and a function Maybe a -> (a -> Maybe b) -> Maybe b
to hide all of the option-unwrapping conditionals.
Remembering this pattern, I think it’s possible to go back and tidy up my error cases:
NSError *error = nil;
id anotherThing = [[[anObject giveMeAThing:&error]
doYourThing:&error]
someSortOfThing:&error];
if (!anotherThing) {
[self handleError:error];
}
Done. Whichever method goes wrong sets the error and returns nil
. Everything after that is sunk, which crucially means that it can’t affect the error. As long as the errors generated are specific enough to indicate what went wrong, whether it’s possible to recover (and if so, how) and whether anything needs cleaning up (and if so, what) then this approach is…good enough.
See also http://fsharpforfunandprofit.com/posts/recipe-part2/
Railway-oriented programming was one of the references in Saul’s talk.