Item 21: Understand the Objective-C Error Model
Many modern languages, including Objective-C, have exceptions. If you have come from a Java background, you’ll most likely be accustomed to using exceptions to handle error cases. If you’re used to using exceptions for this task, you are going to have to go back and forget everything you knew about exceptions and start again.
The first thing to note is that Automatic Reference Counting (ARC, see Item 30) is not exception safe by default. In practice, this means that any objects that should be released at the end of a scope in which an exception is thrown will not be released. It is possible to turn on a compiler flag to enable exception-safe code to be generated, but it introduces extra code that has to be run even for the scenario in which no exception is thrown. The compiler flag that turns this on is -fobjc-arc-exceptions
.
Even when not using ARC, it is difficult to write code that is safe against memory leaks when exceptions are used. Suppose that a resource is created and needs to be released once it is no longer needed. If an exception is thrown before the resource has been released, that release will never be done:
id someResource = /* ... */;
if ( /* check for error */ ) {
@throw [NSException exceptionWithName:@"ExceptionName"
reason:@"There was an error"
userInfo:nil];
}
[someResource doSomething];
[someResource release];
Of course, the way to solve this problem is to release someResource
before throwing; however, if there are many resources to release and more complicated code paths, the code easily becomes cluttered. Also, if something is added to such code, it would be easy to forget to add releases before all times an exception is thrown.
Objective-C has taken the approach recently to save exceptions for the rare scenario in which recovery should be avoided and an exception should cause an application to exit. This means that complex exception-safe code does not need to be involved.
Remembering that exceptions are to be used for fatal errors only, an example of a scenario in which you should consider throwing an exception in your own classes is when creating an abstract base class that should be subclassed before being used.Objective-C has no language construct to say that a class is abstract, unlike some other languages. So the best way to achieve a similar effect is to throw an exception in any method that must be overridden in subclasses. Anyone who then tries to create an instance of the abstract base class and use it will get an exception thrown:
- (void)mustOverrideMethod {
NSString *reason = [NSString stringWithFormat:
@"%@ must be overridden",
NSStringFromSelector(_cmd)];
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
But if exceptions are to be used only for fatal errors, what about other errors? The paradigm chosen by Objective-C to indicate nonfatal errors is either to return nil / 0 from methods where an error has occurred or to use NSError
. An example of returning nil / 0 is when in an initializer and an instance cannot be initialized with the parameters passed in:
- (id)initWithValue:(id)value {
if ((self = [super init])) {
if ( /* Value means instance can't be created */ ) {
self = nil;
} else {
// Initialize instance
}
}
return self;
}
In this scenario, if the if
statement determines that the instance can’t be created with the value passed in—maybe value
needs to be non-nil itself—self
is set to nil
, and this is what will be returned. A caller of the initializer will understand that there has been an error, because no instance will have been created.
Using NSError
provides much more flexibility because it enables a reason to be given back as to what the error is. An NSError
object encapsulates three pieces of information:
Error domain (String)
The domain in which the error occurred. This is usually a global variable that can be used to uniquely define the source of the error. The URL-handling subsystem uses the domain NSURLErrorDomain,
for example, for all errors that come from parsing or obtaining data from URLs.
Error code (Integer)
A code that uniquely defines within a certain error domain what error has occurred. Often, an enum
is used to define the set of errors that can occur within a certain error domain. HTTP requests that fail might use the HTTP status code for this value, for example.
User info (Dictionary)
Extra information about the error, such as a localized description and another error representing the error that caused this error to occur, to allow information about chains of errors to be represented.
The first way in which errors are commonly used in API design is through the use of delegate protocols. When an error occurs, an object may pass its delegate the error through one of the protocol’s methods. For example, NSURLConnection
includes the following method as part of its delegate protocol, NSURLConnectionDelegate
:
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
When a connection decides that there is an error, such as the connection to the remote server times out, this method is called handing an error representing what happened. This delegate method doesn’t have to be implemented, so it is up to the user of the NSURLConnection
class to decide it is necessary to know about the error. This is preferable to throwing an exception, since it’s left up to the user to decide whether to be told about the error.
The other common way in which NSError
is used is through an out-parameter passed to a method. It looks like this:
- (BOOL)doSomething:(NSError**)error
The error variable being passed to the method is a pointer to a pointer to an NSError
. Or you can think of it as a pointer to an NSError
object. This enables the method to, in effect, return an NSError
object in addition to its return value. It’s used like this:
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) {
// There was an error
}
Often, methods that return an error like this also return a Boolean to indicate success or failure so that you can check the Boolean if you don’t care about what the precise error was or, if you do care, you can check the returned error. The error parameter can also be nil
when you don’t care what the returned error is. For instance, you might use the method like this:
BOOL ret = [object doSomething:nil];
if (ret) {
// There was an error
}
In reality, when using ARC, the compiler translates the NSError**
in the method signature to NSError*__autoreleasing*;
this means that the object pointed to will be autoreleased at the end of the method. The object has to do this because thedoSomething:
method cannot guarantee that the caller will be able to release theNSError
object it created and therefore must add in an autorelease
. This gives the same semantics as return values from most methods (excluding, of course, methods that begin with new
, alloc
, copy,
and mutableCopy
).
The method passes back the error through the out-parameter like this:
- (BOOL)doSomething:(NSError**)error {
// Do something that may cause an error
if ( /* there was an error */ ) {
if (error) {
// Pass the 'error' through the out-parameter
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
}
return NO; ///< Indicate failure
} else {
return YES; ///< Indicate success
}
}
The error
parameter is dereferenced using the *error
syntax, meaning that the value pointed to by error
is set to the new NSError
object. The error
parameter must first be checked to see whether it is non-nil, since dereferencing the null pointer will result in a segmentation fault and cause a crash. Since it is fine for a caller to pass nil, this check must be made for the case that it doesn’t care about the error,
The domain, code, and user information portions of the error object should be set to something that makes sense for the error that has happened. This enables the caller to behave differently, depending on the type of error that has occurred. The domain is best defined as a global constant NSString,
and the error codes are best defined as an enumeration type. For example, you might define them like this:
// EOCErrors.h
extern NSString *const EOCErrorDomain;
typedef NS_ENUM(NSUInteger, EOCError) {
EOCErrorUnknown = –1,
EOCErrorInternalInconsistency = 100,
EOCErrorGeneralFault = 105,
EOCErrorBadInput = 500,
};
// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";
Creating an error domain for your library is prudent, since it allows you to create and return NSError
objects that consumers can ascertain came from your library. Creating an enumeration type for the error codes is also a good idea, since it documents the errors and gives the codes a meaningful name. You may also decide to comment the header file where they are defined with even more detailed descriptions of each error type.
Things to Remember
Use exceptions only for fatal errors that should bring down the entire application.
For nonfatal errors, either provide a delegate method to handle errors or offer an out-parameter NSError
object.