7

Many Cocoa methods take an optional NSError ** argument that they use to report errors. Frequently, I find myself using such methods even in circumstances where the only way that an error could occur is through programming error on my part, rather than unexpected runtime conditions. As such, I don't want to write any error-handling code that will do any user-visible things. All I really want to do upon error is log the error (and perhaps crash), while keeping my code as concise and readable as possible.

The trouble is that the 'keep the code concise' and 'log the error' objectives are in tension with each other. I frequently have a choice between these two approaches, both of which I dislike:

1. Pass NULL for the error pointer argument.

[managedObjectContext save:NULL];
  • Advantages: Succinct, readable, and makes clear that errors are not expected. Perfectly fine as long as I was correct in my belief that an error here is logically impossible.
  • Disadvantages: If I've screwed up and an error does happen, it won't be logged and my debugging will be harder. In some circumstances, I might not even notice the error happened.

2. Pass an NSError ** and log the resulting error, with the same boilerplate code, every single time.

NSError *error;
[managedObjectContext save:&error];
if (error) {
    NSLog(@"Error while saving: %@", error);
}
  • Advantages: Error don't pass silently - I'm alerted to them and given debugging information.
  • Disadvantages: It's horribly verbose. It's slower to write, slower to read, and when it's nested within some levels of indentation already, I feel that it makes the code less readable. Routinely doing this just for logging errors, and getting used to skipping over the boilerplate while reading, also makes me sometimes fail to notice when some code I'm reading actually has significant error handling blocks for errors that are expected to happen at runtime.

Coming from a background of languages like Python, Java, PHP and Javascript, I find it kind of cumbersome to have to write 4 extra lines of boilerplate to get notified of the kind of errors that, in the languages I'm used to, I'd find out about through an exception or warning without ever having to write any code that explicitly checks for errors.

What I'd ideally like is some cunning hack that I can use to automatically log the errors created by these methods without needing to write the boilerplate on every method call, thus giving me the benefits of both the lazy NULL-passing approach and the error-logging boilerplate. In other words, I'd like to write this:

[managedObjectContext save:&magicAutologgingError];

and know that if the method created an NSError, it would somehow, magically, be logged.

I'm not too sure how to go about this. I considered using an NSError subclass that logs itself upon dealloc, but realised that since I'm not responsible for instantiating the error objects that Cocoa's methods create, my subclass wouldn't be used anyway. I considered using method swizzling to make all NSErrors log themselves on dealloc like this, but I'm not sure if that would actually be desirable. I thought about using some sort of observer class that watches a given constant space in memory that I could use for the NSError pointers that I want to log, but as far as I know there's no way to do anything like KVO for observing arbitrary spaces in memory, so I can't see an approach for implementing this other than having a thread that repeatedly checks for errors to log.

Can anyone see a way to achieve this?

Mark Amery
  • 110,735
  • 57
  • 354
  • 402
  • 1
    I find it curious that you state ‘the only way that an error could occur is through programming error on my part’. In Cocoa, exceptions are used in this case, not errors. In your particular example, `-save:` could return `NO` if the user has decided to place the Core Data store in a remote location which has become unavailable. –  Sep 28 '13 at 13:00
  • 1
    FWIW, passing `NULL` doesn't "make clear that errors are not expected", it makes it clear that you don't care to know about any errors that occur. – ipmcc Sep 28 '13 at 13:32
  • @Bavarious The point I was trying to make with that sentence - and perhaps I phrased it badly - was that just because *in theory*, when calling a particular method, an error could occur that was the result of expected runtime conditions and not programmer error, doesn't mean that *in your particular program, with your particular call to that method* any such possibility exists. – Mark Amery Sep 28 '13 at 13:40
  • @Bavarious Perhaps `NSManagedContext save:` was a bad example, but a better one would be `NSRegularExpression regularExpressionWithPattern:`, which takes an error argument. Now, sure, in theory you could be letting the user specify regex patterns and those could be invalid, I guess, in which case you'd expect an error here at runtime and want to handle it. But if you're using a string literal as the pattern (which I have been every single time I've used this method), then the only way an error can ever occur is through programmer error. – Mark Amery Sep 28 '13 at 13:41
  • @Bavarious This is directly analagous to how it's sometimes appropriate in Java to catch a checked exception (which are supposed to be used for expected error conditions at runtime) and throw an unchecked exception (which are supposed to be used to indicate programmer error) because in the particular circumstances of the call programmer error is the only possible explanation for that checked exception being raised. – Mark Amery Sep 28 '13 at 13:43
  • 2
    Note that `if (error) {` is incorrect; you cannot test `error` to know if an `error` was generated. Your example of `NSRegularExpression` is slightly off the mark; that API defines that an erroneous input is recoverable, regardless of whether it is static or not. Thus, is uses `NSError` because those encapsulate recoverable errors whereas `NSException` should always be treated as a fatal programmer error. – bbum Sep 28 '13 at 16:58
  • 1
    @bbum Yeah, thanks for the note about `if (error)`. I actually saw *you* make this point elsewhere while reading around after posting this question, and JeremyP has also made the same point in his answer. (Although that said, some method docs - like those for `NSRegularExpression` init and constructor methods - specify that a `nil` error indicates success and do not specify what return value would indicate failure. Are you sure that what you've asserted here is still uniformly true across Cocoa?) – Mark Amery Sep 28 '13 at 17:04
  • @MarkAmery There are a handful of exceptions and they are all bugs. Sounds like the `NSRegularExpression documentation should be clarified. It *may* be the case that an expression parses and produces something that can be useful, but that it may also be ambiguous. – bbum Sep 28 '13 at 23:54
  • Aha, `NSRegularExpression` is indeed a better example for what you want. Personally, I think I’d use categories to wrap Cocoa methods whose errors I’m not interested in dealing with. –  Oct 01 '13 at 09:53

4 Answers4

1

One problem with swizzling -[NSError dealloc] to log errors is that you still have to pass a pointer to an NSError, otherwise there's no guarantee that the error will ever be created. For instance, it seems plausible that various framework methods might be implemented like this:

if (outError)
{
    *outError = [[[NSError alloc] init] autorelease]; // or whatever.
}

You could make a global pointer, say:

NSError* gErrorIDontCareAbout = nil; 
NSError** const ignoredErrorPtr = &gErrorIDontCareAbout;

...and declare it as extern in your prefix header and then pass ignoredErrorPtr to any method whose error you don't care to present, but then you lose any locality in terms of where the error occurred (and really this will only work right if you use ARC).

It occurs to me that what you really want to do is swizzle the designated initializer (or allocWithZone:) and dealloc and in that swizzled/wrapped method, call [NSThread callStackSymbols] and attach the returned array (or perhaps its -description) to the NSError instance using objc_setAssociatedObject. Then in your swizzled -dealloc you can log the error itself and the call stack where it originated.

But any way you do it, I don't think you can get anything useful if you just pass NULL, because the frameworks are free to not create the NSError in the first place if you've told them you're uninterested in it (by passing NULL).

You might do it like this:

@implementation MyAppDelegate

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Stash away the callstack
        IMP originalIMP = class_getMethodImplementation([NSError class], @selector(initWithDomain:code:userInfo:));
        IMP newIMP = imp_implementationWithBlock(^id(id self, NSString* domain, NSInteger code, NSDictionary* dict){
            self = originalIMP(self, @selector(initWithDomain:code:userInfo:), domain, code, dict);
            NSString* logString = [NSString stringWithFormat: @"%@ Call Stack: \n%@", self, [NSThread callStackSymbols]];
            objc_setAssociatedObject(self, &onceToken, logString, OBJC_ASSOCIATION_RETAIN);
            return self;
        });
        method_setImplementation(class_getInstanceMethod([NSError class], @selector(initWithDomain:code:userInfo:)), newIMP);

        // Then on dealloc... (Note: this assumes that NSError implements -dealloc. To be safer you would want to double check that.)
        SEL deallocSelector = NSSelectorFromString(@"dealloc"); // STFU ARC
        IMP originalDealloc = class_getMethodImplementation([NSError class], deallocSelector);
        IMP newDealloc = imp_implementationWithBlock(^void(id self){
            NSString* logString = objc_getAssociatedObject(self, &onceToken);
            if (logString.length) NSLog(@"Logged error: %@", logString);
            originalDealloc(self, deallocSelector); // STFU ARC
        });
        method_setImplementation(class_getInstanceMethod([NSError class], deallocSelector), newDealloc);
    });
}

@end

Note that this will log all errors, not just the ones you don't handle. That may or may not be acceptable, but I'm struggling to think of a way to make the distinction after the fact without making some call everywhere you do handle the error.

ipmcc
  • 28,584
  • 4
  • 78
  • 135
  • Just got round to taking a proper look at this (sorry - I haven't played with swizzling before and this was too hard for me to get my head round without some time to sit down and think and play with it). It seems to almost work (it logs what it's supposed to!), but it's crashing with an `EXC_BAD_ACCESS` whenever `originalDealloc(self, deallocSelector);` gets called, and I have no idea why. Any ideas? – Mark Amery Oct 05 '13 at 21:29
1

Just create a wrapper function (or category method) which does what you want it to:

bool MONSaveManagedObjectContext(NSManagedObjectContext * pContext) {
 NSError * error = nil;
 bool result = [pContext save:&error];
 if (!result && nil != error) {
  // handle  the error how you like -- may be different in debug/release
  NSLog(@"Error while saving: %@", error);
 }
 return result;
}

and call that instead. Or you might prefer to make error handling separate:

void MONCheckError(NSError * pError, NSString * pMessage) {
 if (nil != pError) {
  // handle  the error how you like -- may be different in debug/release
  NSLog(@"%@: %@", pMessage, pError);
 }
}

...

NSError * outError = nil;
bool result = [managedObjectContext save:&outError];
MONCheckError(outError, @"Error while saving");

Always be on the lookout for duplicate code :)


I considered using method swizzling to make all NSErrors log themselves on dealloc like this, but I'm not sure if that would actually be desirable.

It's not desirable.

justin
  • 101,751
  • 13
  • 172
  • 222
  • Creating wrappers for every Cocoa method that takes an error pointer argument seems like overkill for the scale of projects that I work on (although if there were a library providing these wrappers, I would use it). Having some kind of an `errorLog` function that logs non-nil errors and does nothing on `nil` errors might be a handy and simple way of trimming down the boilerplate a little, though (although @bbum would object to this on the grounds that a non-`nil` error isn't guaranteed to indicate that an error occurred for many methods). – Mark Amery Sep 28 '13 at 17:44
  • 1
    @MarkAmery it doesn't have to be *every* API (also, that would be huge if you opted for categories). just add them as you need - the additional lines required to wrap pays itself off by the time you have used the function as a substitute 2 or 3 times. – justin Sep 28 '13 at 18:45
1

One approach is that you could define a block that takes an NSError ** parameter, put your error producing expressions inside such blocks (passing the block parameter as the error parameter to the code), and then write a function that executes blocks of that type, passing and logging an error reference. For example:

// Definitions of block type and function
typedef void(^ErrorLoggingBlock)(NSError **errorReference);

void ExecuteErrorLoggingBlock(ErrorLoggingBlock block)
{
    NSError *error = nil;
    block(&error);
    if (error) {
        NSLog(@"error = %@", error);
    }
}

...

// Usage:
__block NSData *data1 = nil;
ErrorLoggingBlock block1 = ^(NSError **errorReference) {
    data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.google.com"] options:0 error:errorReference];
};
__block NSData *data2 = nil;
ErrorLoggingBlock block2 = ^(NSError **errorReference) {
    data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://wwwwwlskjdlsdkjk.dsldksjl.sll"] options:0 error:errorReference];
};

ExecuteErrorLoggingBlock(block1);
ExecuteErrorLoggingBlock(block2);

NSLog(@"data1 = %@", data1);
NSLog(@"data2 = %@", data2);

If that's still too verbose, you might consider either some preprocessor macros or some Xcode code snippets. I found the following set of macros to be reasonably resilient:

#define LazyErrorConcatenatePaste(a,b) a##b
#define LazyErrorConcatenate(a,b) LazyErrorConcatenatePaste(a,b)
#define LazyErrorName LazyErrorConcatenate(lazyError,__LINE__)
#define LazyErrorLogExpression(expr) NSError *LazyErrorName; expr; if (LazyErrorName) { NSLog(@"error: %@", LazyErrorName);}

Usage was like this:

LazyErrorLogExpression(NSData *data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://google.com"]
                                                             options:0
                                                               error:&LazyErrorName])
NSLog(@"data1 = %@", data1);
LazyErrorLogExpression(NSData *data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://sdlkdslkdsslk.alskdj"]
                                                             options:0
                                                               error:&LazyErrorName])
NSLog(@"data2 = %@", data2);

The macro uniqued the error variable name, and was resistant to the method call line breaks. If you're really adamant about not having any extra lines of code in your source file, then this may be the safest approach - it doesn't involve throwing unusual exceptions or swizzling framework methods, at least, and you can always view the preprocessed code in the assistant editor.

I'd argue, however, that the verbosity of Objective-C is intentional, particularly with respect to increasing the ease of reading and maintaining code, so a better solution may be to make a custom Xcode snippet. It won't be as fast to write as using the macros above (but with keyboard shortcuts and autocompletion it will still be really quick), but it will be absolutely clear to future readers. You can drag the following text to the snippet library and define a completion shortcut for it.

NSError *<#errorName#>;
<#expression_writing_back_error#>;
if (<#errorName#>) {
    NSLog(@"error: %@", <#errorName#>);
}

One final disclaimer: these patterns should only be used for logging, and the return value should actually determine success or failure in the case of error recovery. Although, the block based approach could be fairly easily made to return a boolean indicating success or failure if some common error recovery code was needed.

Carl Veazey
  • 18,241
  • 8
  • 62
  • 80
0

If the error that occurs can truly only be caused by a programming error, I'd throw an exception because the chances are you want the program to stop anyway,

What I do is have an exception class that takes the error as a parameter in its init. You can then populate the exception info with stuff from the error e.g.

-(id) initWithError: (NSError*) error
{
    NSString* name = [NSString stringWithFormat: @"%@:%ld", [error domain], (long)[error code]];
   self = [super initWithName: name
                       reason: [error localizedDescriptionKey]
                     userInfo: [error userInfo]];
   if (self != nil)
   {
       _error = error;
   }
   return self;
}

Then you can also override -description to print out some of the relevant error info.

The correct way to use this is

NSError *error = nil;
if (![managedObject save:&error])
{
    @throw [[ErrorException alloc] initWithError: error];
}

Note, by the way that I detect the error by testing the result of sending the message not by seeing if the error is nil. It's a convention in Objective-C not to use the error pointer itself to detect if there is an error at all, but to use the return value.

ipmcc
  • 28,584
  • 4
  • 78
  • 135
JeremyP
  • 80,230
  • 15
  • 117
  • 158
  • @Mark Amery It is more than a convention to not test for an error by looking at the error parameter, this is because there is no guarantee that the error parameter will be nil on correct execution. There is the possibility that the error parameter is set to some value during execution of the method when in the end there is no error. – zaph Sep 28 '13 at 12:55