What are the best practices for exceptions/returning NO/nil in Objective-C?
Exceptions should be used as little as possible in Objective-C. Where other languages would use exceptions, in Objective-C it's recommended to make use of NSError objects most of the time.
Apple's documentation on exception handling is here: http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/Exceptions/Exceptions.html%23//apple_ref/doc/uid/10000012il
So how would one make use of NSError objects? Well, if we look at Apple's classes, errors are returned using an indirection pointer.
For example:
- (NSObject *)objectFromSet:(NSSet *)set error:(NSError **)error
{
// get an object from a set; if the set has at least 1 object
// we return an object, otherwise an error is returned.
NSObject *object = [set anyObject]
if (!object)
{
*error = [NSError errorWithDomain:@"AppDomain" code:1000 userInfo:nil];
return nil;
}
return object;
}
// and then we use the function like this
- (void)test
{
NSError *error = nil;
NSSet *set = [[[NSSet alloc] init] autorelease];
NSObject *object = [self objectFromSet:set error:&error];
if (object)
{
// use the object, all went fine ...
}
else
{
// handle error, perhaps show an alert view ...
}
}
I won't be definitive about which to use, but here's some info about each of the options:
Exceptions
Exceptions in Obj-C are not really meant to be used to control program flow. From the documentation on exception handling:
The general pattern is that exceptions are reserved for programmer error only, and the program catching such an exception should quit soon afterwards.
For this reason I wouldn't recommend using exceptions @try
/@catch
just to test whether a method worked correctly.
You also have several options for handling exceptions, in addition to setting a higher-level uncaught exception handler.
Errors
Errors are typically used in three ways:
Delegate methods
An object can simply pass an NSError to its delegate in a designated error-handling callback:
- (void)myObject:(MyObject *)obj didFailWithError:(NSError *)error;
The delegate is then free to take any appropriate action, including perhaps displaying a message to the user. This pattern is commonly used in asynchronous delegate-based APIs.
Out parameters
These are most commonly used in conjunction with a boolean return value: if the return value is NO
, then the NSError object can be examined for more information about the error.
- (BOOL)performTaskWithParameter:(id)param returningError:(out NSError **)error;
Where one possible usage pattern would be:
NSError *error;
if (![myObject performTaskWithParameter:@"param" returningError:&error]) {
NSLog(@"Task failed with error: %@", error);
}
(Some people also prefer to store the boolean result in a variable before checking it, such as BOOL success = [myObject perform...];
.) Due to the linear nature of this pattern, it's best used for synchronous tasks.
Block-based completion handlers
A fairly recent pattern since the introduction of blocks, yet a quite useful one:
- (void)performAsynchronousTaskWithCompletionHandler:(void (^)(BOOL success, NSError *error))handler;
Used like this:
[myObject performAsynchronousTaskWithCompletionHandler:^(BOOL success, NSError *error) {
if (!success) {
// ...
}
}];
This varies a lot: sometimes you won't see the boolean parameter, just the error; sometimes the handler block has no arguments passed to it and you just check a state property of the object (for example, this is how AVAssetExportSession works). This pattern is also great for asynchronous tasks, when you want a block-based approach.
Handling errors
Cocoa on Mac OS X has a quite thorough error-handling path. There is also NSAlert's convenience method + (NSAlert *)alertWithError:(NSError *)error;
. On iOS, the NSError class still exists, but there aren't the same convenience methods to handle errors. You may have to do a lot of it yourself.
Read the Error Handling Programming Guide for more information.
Returning nil
This is often used in conjunction with NSError out parameters; for example, NSData's method
+ (id)dataWithContentsOfFile:(NSString *)path
options:(NSDataReadingOptions)mask
error:(NSError **)errorPtr;
If reading the file fails, this method returns nil
, and further information is stored in an error.
One reason this is a particularly convenient pattern is because of nil messaging, which can be done safely with no effect in Obj-C. I won't go into detail here on why this is useful, but you can read more about it elsewhere on the interwebs. (Just make sure to find an up-to-date article; it used to be that methods returning floating-point values wouldn't necessarily return 0 when sent to nil, but now they do, as described in the documentation.)