Effective Objective-C 2.0: Item 21: Understand the Objective-C Error Model

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:

Image 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.

Image 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.

Image 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 newalloccopy, 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(NSUIntegerEOCError) {
    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

Image Use exceptions only for fatal errors that should bring down the entire application.

Image For nonfatal errors, either provide a delegate method to handle errors or offer an out-parameter NSError object.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值