views:

740

answers:

3

I'm coding up various routines and I'm trying my best to keep it neat and refactored.

Methods I'm creating are starting to look similar to this code:

-(IBAction)buttonPress:(id)sender {
    // Create Document Shopping List with this document
    [self doSomething:&error];

    if(error) {
        [NSApp presentError:&error];
        return nil;
    }

    [self doSomethingElse:&error];

    if(error) {
        [NSApp presentError:&error];
        return nil;
    }

    [self doYetSomethingElse:&error];

    if(error) {
        [NSApp presentError:&error];
        return nil;
    }
}

I love NSError, but this seems like an awfully clumsy way of handling all my errors.

A few thoughts I've had about alternative ways:

a) the error checking could be built into the methods doSomething, doSomethingElse etc, but then I wouldn't be able to exit the button press method without doing some kind of checking on the return value, which would lead me back to a similar structure.

b) I could set up the NSError as being Key Value Observed, but something about this feels deeply wrong. I'm very aware of the possibilities of abuse with KVO, so I'm trying to do everything without it whereever possible.

Surely I'm missing something really basic here? Is there a pattern that can help me? Or is this structure OK?

+3  A: 

Note: When I posted this I was unaware of any MacOS/Cocoa exceptions debate. Please bear this in mind when you consider this answer.

Why not use exceptions instead? This will allow you to dispense with the error parameter on your methods and handle your errors in one place.

-(IBAction)buttonPress:(id)sender {
    // Create Document Shopping List with this document
    @try {
        [self doSomething];
        [self doSomethingElse];
        [self doYetSomethingElse];
    } @catch (NSException* e) {
        [NSApp presentException:e];
    }
    return nil;
}

You will of course need to throw exceptions as required from within your doXXXX methods:

NSException* e = [NSException exceptionWithName:@"MyException"
                                         reason:@"Something bad happened"
                                       userInfo:nil];
@throw e;
teabot
There is an ongoing debate about whether using exceptions for flow control is acceptable in Mac OS X/Cocoa. I personally take the position that (most of) Apple takes, that exceptions in Cocoa are for exceptional programming errors, and therefore shouldn't be used for flow control.
danielpunkass
Would be interested to know why this was marked down. What have I missed?
teabot
Ah, thanks for clearing it up @danielpunkass - I'm from a Java background so have been happily using exceptions in Objective-C unaware of such a debate.
teabot
I should have mentioned I did briefly consider exceptions, but since the conditions aren't exceptional, my understanding was that NSError was the way to go, as stated by teabot and daniel.
John Gallagher
teabot: It was me who down-voted the answer. I just took the down-vote off because I think with your caveat it reads as a forthright option for anybody to consider.
danielpunkass
@danielpunkass - Thanks - it did actually spur me on to read about how exception handling differs between ObjC and Java, and why care must be taken when considering their use in this environment. So I definitely considered it a constructive down vote!
teabot
My question seems to have triggered some discussion and learning around the exception versus error issue. Wow. I feel all warm and fuzzy now.
John Gallagher
+1  A: 

Hey John,

I think the real question is how much granularity you need in your error handling. Your code is fundamentally correct - but you can clean things up by checking for errors less frequently, or using a catch-all approach like teabot mentioned in his answer with NSException. A lot of the built-in functions that return errors (like NSFileManager's moveItemAtPath:) return a BOOL in addition to a providing an NSError object - so you could also use nested if statements if you don't need the NSError information.

All and all though, there's really not a great way to do this. If your code is long, KVO might be a cool solution. I've never tried it for a situation like this, though.

Ben Gotow
Thanks for the comment, Ben. Ideally, I'd like to be able to handle all errors one consistent way - by using NSError. I'm somewhat confused by the functions that sometimes do return a BOOL - if there's a standard way of handling errors using NSError, why would Apple muddy the waters by providing return values as well? That's always confused me a bit.
John Gallagher
Relying on an NSError object is, frankly, foolish. It's very easy for one to miss that one edge case where at the time writing the code to create an error object was too much hassle, and as a result, the method wrongly reports success.This is why Apple handles error information the way they do. Use a simple test of a method returning NO or nil to determine that it failed. THEN drop down the to the NSError object (if it's available) for further details.
Mike Abdullah
Very good point, Mike. Gives me something else to consider. Only just read this, I mustn't have checked SO promptly enough.
John Gallagher
+2  A: 

The gist of your question is whether there are structural improvements you can make to your error handling. I think so, by essentially introducing more layers of nesting, either by extracting more code into separate methods/functions, or by introducing nesting in your high level sample method.

The idea is, when it comes to handling most errors, you probably are either interested in performing an alternate task, or in failing and propagating the error up the chain so that some responsible controller can convey the error to the user through UI.

Using this idea of "propagate or handle", I would rewrite your sample method like this:

-(IBAction)buttonPress:(id)sender {

    // Create Document Shopping List with this document
    [self doSomething:&error];    
    if(error == nil) {
        [self doSomethingElse:&error];
        if (error == nil) {
            [self doYetSomethingElse:&error];
        }
    }

    if(error) {
        [NSApp presentError:&error];
    }    
}

Note that there are good arguments against introducing too much nesting in a particular method. Nesting such as this is essentially a short alternative to extracting methods. It might make more sense, for instance, that "doSomething:" itself calls doSomethingElse:, which calls doYetSomethingElse: for instance. This would impose the same structure on the code as the if-nest, but would be arguably more maintainable.

As an aside, I am not a fan of inline return statements. In this particular instance, the sample method doesn't actually call for a return value, but if it did, I prefer to set a local variable to the returned value and only return at the end of the flow control. Jumping out of a function or method prematurely is a sure way to encounter weird bugs, IMHO.

danielpunkass
Thanks for an excellent suggestion, Daniel. I shied away from doing this because Code Complete recommends minimising nesting. I've made a few nightmare nested if.. else statements in my time. And if I had more than 5 steps, I guess this would become cumbersome.On the other hand, if I had more than 5 steps, I guess it would be a warning sign that my code needed to be refactored anyway. And this specific nesting is still pretty readable and is much neater than my original, so this is what I'll do.Thanks again!
John Gallagher
Glad you appreciated the answer. I also think of Code Complete and minimizing nesting, but I rationalize it the way I did above, to consider that minimum nesting is just a shorthand for code that can be factored into another method or function when circumstances call for it.Generally, I think nesting is better than the alternative, a "run-on" function...
danielpunkass