Less detailed way of reporting errors in Objective-C

Many Cocoa methods accept the optional NSError ** argument, which they use to report errors. Often I find myself using such methods even in cases where the only way that an error can occur is a programming error on my part, and not unexpected execution conditions. Thus, I do not want to write error handling code that will do any things that the user sees. All I really want to do with an error is to register an error (and possibly crash), keeping my code as concise and readable as possible.

The problem is that the tasks of "keeping the code concise" and the "error log" are in tension with each other. I often have a choice between these two approaches, both of which I don't like:

1. Pass NULL for the error pointer argument.

 [managedObjectContext save:NULL]; 
  • Advantages: Clear, readable, and makes it clear that no errors are expected. Absolutely fine, while I was right in believing that a mistake here is logically impossible.
  • Disadvantages: if I mess up and an error occurs, it will not be logged, and my debugging will be more difficult. In some cases, I might not even notice that an error has occurred.

2. Pass NSError ** and register the received error with the same code template each time.

 NSError *error; [managedObjectContext save:&error]; if (error) { NSLog(@"Error while saving: %@", error); } 
  • Advantages: the error does not pass silently - I warn them and get debugging information.
  • Minuses: This is terribly verbose. It is slower to write, slower to read, and when it is already nested in some levels of indentation, I feel that it makes the code less readable. Regularly doing this just to register errors and getting used to skipping the template while reading, also makes me sometimes not notice when some of the code that I am reading actually has significant error handling blocks for errors that are expected to be executed during lead time.

Based on the background of languages ​​such as Python, Java, PHP, and Javascript, I find it quite difficult to write 4 additional lines of the template to receive notifications about the errors that are in the languages ​​that I use, I would learn about it through an exception or warning, without requiring writing code that explicitly checks for errors.

I would like it to be a tricky hack that I can use to automatically register errors generated by these methods, without having to write boilerplate code every time the method is called, which gives me advantages as a lazy approach of NULL traversal and an error registration pattern. In other words, I would like to write the following:

 [managedObjectContext save:&magicAutologgingError]; 

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

I'm not too sure how to do this. I looked at using the NSError subclass that is registered on dealloc , but I realized that since I am not responsible for instantiating the error objects generated by Cocoa methods, my subclass will not be used anyway. I have considered using the swizzling method so that all NSError written to dealloc this way, but I'm not sure if that would really be desirable. I was thinking of using some kind of observer class that observes this constant memory space that I could use for the NSError pointers I want to register, but as far as I know, there is no way to do something like KVO to observe arbitrary there are no memory spaces, so I don’t see an approach to implement this, except that there is a thread that repeatedly checks for errors for the log.

Can anyone see a way to achieve this?

+6
source share
4 answers

Just create a wrapper function (or category method) that does what you want:

 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 name it instead. Or you might prefer to handle errors separately:

 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 code duplication :)


I have considered using the swizzling method so that all NSErrors are logged on dealloc like this, but I'm not sure if that would really be desirable.

This is undesirable.

+1
source

One problem with the error -[NSError dealloc] log log is that you still need to pass a pointer to NSError, otherwise there is no guarantee that the error will ever be generated. For example, it seems plausible that various framework methods can be implemented as follows:

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

You can make a global pointer, say:

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

... and declare it as extern in the prefix header, and then pass ignoredErrorPtr any method whose error you do not want to represent, but then you lose some locale in terms of where the error occurred (and in fact it will work correctly, 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 this swizzled / wrapped method call [NSThread callStackSymbols] and attach the returned array (or maybe its -description ) to an NSError instance using objc_setAssociatedObject . Then in your swizzled -dealloc you can register the error itself and the call stack where it came from.

But no matter how you do it, I don’t think you can get anything useful if you just pass NULL , because frameworks may not create NSError in the first place, if you told them you were not interested in it (passing NULL ) .

You can do it as follows:

 @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 

Please note that this will log all errors, not just those that you do not handle. This may or may not be acceptable, but I'm struggling to come up with a way to make a distinction after the fact without any challenge in everything that you handle the error.

+1
source

One approach is that you can define a block that accepts an NSError ** parameter, put your error by generating expressions inside such blocks (passing the block parameter as an error parameter to the code), and then write a function that executes blocks of this type by passing and registering a link to the error. For instance:

 // 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 this is still too much, you may want to consider some preprocessor macros or some pieces of Xcode code. I found that the following set of macros is fairly robust:

 #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);} 

The use was as follows:

 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 uniquely assigned a name to the error variable and was resistant to breaking the method call line. If you are really not sure about the absence of any additional lines of code in the source file, then this may be the safest approach - it does not involve throwing unusual exceptions or a swizzling framework, at least, and you can always view the pre-processed code in the editor’s assistant .

I would say, however, that the verbosity of Objective-C is intentional, especially with respect to improving readability and maintaining code, so creating a custom Xcode snippet might be the best solution. It’s not so fast to write how to use the macros above (but with keyboard shortcuts and autocomplete it will still be very fast), but it will be absolutely clear to future readers. You can drag the following text into the fragment library and define a completion label for it.

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

One final disclaimer: these templates should only be used for logging, and the return value should actually indicate success or failure in the event of error recovery. Despite the fact that a block-based approach could be fairly easy to return to a boolean indicating success or failure if a general error recovery code is needed.

+1
source

If the error that occurred could really be caused by a programming error, I would choose an exception, because most likely you want the program to stop anyway,

I have an exception class that accepts an error as a parameter in init. Then you can fill in the exception information with error information, for example.

 -(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 relevant error information.

The right way to use this

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

Please note that I found an error by testing the result of sending a message without seeing that the error is zero. This convention in Objective-C does not use the error pointer itself to detect if there is any error at all, but use the return value.

0
source

Source: https://habr.com/ru/post/954834/


All Articles