Whatever the cause, product errors make an application unpleasant to use if they are not handled properly. They can lead to a crash or freeze during normal use, and they can endanger user data. Users become frustrated and developers look incompetent. To guard against product errors, developers should make Cocoa projects error-tolerant. This means being able to encapsulate an error as it occurs, to dispatch it, and to respond to it properly. Cocoa provides us with the tools to accomplish this in the form of the NSError and NSResponder classes. In this article, I examine how to create an error object and how to dispatch it on the application's event loop. I will also explore how to respond to the error, as well as how to alter it, and finally, how to assign the right recovery routine to an error.
To follow, you'll need a basic understanding of Objective-C. All featured code is applicable to both OS X and iOS projects, unless stated otherwise.
The Error Object
The NSError class (Figure 1) serves as the basis for an error object. It lets the error be identified and categorized. It can carry a collection of localized strings needed to describe the error. It can even invoke a recovery routine for the given error.
Figure 1.
NSError offers two ways to create an error object. One way is with the factory method errorWithDomain:code:userInfo:. This method takes three input arguments: the error domain, its unique code ID, and a support dictionary. It can accept a nil in place of the support dictionary. It marks the error object for autorelease for easy disposal.
NSString *eTyp; NSInteger *eID; NSError *eErr; eTyp = [NSString stringWithString:@"FooErrType"]; eID = 0xf00; eErr = [NSError errorWithDomain:eTyp code:eID userInfo:nil];
Another way to create the NSError object is with the instance method initWithDomain:code:userInfo:. This also takes the same three arguments as the factory method. It does not, however, mark the resulting error object for autorelease. It should be used together with an alloc message. The error object should be disposed of with a release message.
eErr = [[NSError alloc] initWithDomain:eTyp code:eID userInfo:nil]; //... [eErr release];
The NSError class uses three accessors to retrieve parts of the error data. The accessor code returns the code ID as an NSInteger. The accessor domain returns the error domain as an NSString. And the accessor userInfo returns the support dictionary as an NSDictionary object.
NSError also has accessors for the support dictionary. For instance, the accessor localizedDescription returns a concise description of the error. The accessor localizedFailureReason gives the cause of that error. And the accessor localizedRecoverySuggestion describes the recovery routine.
The accessor localizedRecoveryOption returns an NSArray object. This array object carries one or more text objects used for populating an alert dialog. The accessor recoveryAttempter returns a pointer to the ObjC object supplying the recovery routine.
Preparing the Error Object
Creating an NSError object is straightfoward, but how is the data be prepared for the error object? There are three principal steps to the process.
1. Pick the error domain.
An error domain describes the general locale of the error. There are four possible domains. Errors caused by low-level code, such as those from kernel routines, are from the Mach domain (NSMachErrorDomain). Those made by calls to a POSIX library are from the POSIX domain (NSPOSIXErrorDomain). Errors that came from the Carbon library have an OS status domain (NSOSStatusErrorDomain). And those that came from a Cocoa class have a Cocoa domain (NSCocoaErrorDomain). Each error domain gets its own constant NSString object. We can, of course, define our own domain when developing a custom ObjC framework.
2. Choose the appropriate error code.
This is usually a signed integer (NSInteger). Most error codes are declared by the same header files that can be accessed from any C routine. For instance, error codes for the Mach domain are in the file /usr/include/mach/kern_return.h. Those for the POSIX domain are in /usr/include/sys/errno.h. Carbon error codes are declared by the file MacErrors.h, which sits inside the framework bundle CarbonCore.framework. Similarly, Cocoa error codes are declared by their respective frameworks: Foundation, AppKit, Core Data, and so on.
3. Assemble the support dictionary.
The support dictionary provides the information needed to further describe the error. By default, it carries localized strings that can be displayed in an alert dialog or written to a log file. Supplying a support dictionary is optional.
Each dictionary entry is assigned a unique key. Cocoa itself declares five NSString constants to serve as keys for the default entries. The error description gets the constant NSLocalizedDescriptionKey, the failure reason NSLocalizedDescriptionKey. The array object gets NSLocalizedRecoveryOptionsErrorKey, the recovery description NSLocalizedRecoverySuggestionErrorKey. And the optional recovery object gets NSRecoveryAttempterErrorKey.
Listing One demonstrates how to create a basic error object. This sample snippet starts by importing the file Foundation.h. It creates an instance of the Cocoa class NSFileManager (line 9) and declares the path to the file "Foo.text" (lines 10-11). Then it sends a fileExistsAtPath: message to tMgr to check for the file (line 12). If the file exists, the snippet runs the desired file operation. If not, it creates an NSError object, sets the error code to 4 (missing file or NSFileNoSuchFileError), the error domain to NSCocoaErrorDomain (lines 19-21). Notice this error object gets a nil for its support dictionary.
Listing One
#import <Foundation/Foundation.h>
.
.
.
NSFileManager *tMgr;
NSString *tPth;
BOOL tChk;
tMgr = [NSFileManager defaultManager];
tPth = [NSString stringWithString:@"~/Documents/Foo.text"];
tPth = [tPth stringByExpandingTildeInPath];
tChk = [tMgr fileExistsAtPath:tPth];
if (tChk)
{
// perform a file operation...
}
else
{
NSError *eErr;
eErr = [NSError errorWithDomain:NSCocoaErrorDomain
code:4 userInfo:nil];
// dispatch the error object...
Listing Two shows how to prepare the support dictionary. First, the snippet creates an instance of NSMutableDictionary (line 10). It prepares two constant NSString objects and stores them under their respective keys (lines 13-19). Then the snippet creates the NSError object, passing the support dictionary (eData) to the factory method (lines 21-22).
Listing Two
#import <Foundation/Foundation.h> . . . NSMutableDictionary *eData; NSString *eDesc, *eFail; NSError *eErr; // create a dictionary object eData = [NSMutableDictionary dictionaryWithCapacity:2]; // prepare the support dictionary eDesc = [NSString stringWithString:@"Could not locate the file Foo.text."]; [eData setObject:eDesc forKey:NSLocalizedDescriptionKey]; eFail = [NSString stringWithString:@"The file simply did not exists"]; [eData setObject:eFail forKey:NSLocalizedFailureReasonErrorKey]; eErr = [NSError errorWithDomain:NSCocoaErrorDomain code:4 userInfo:eData]; // dispatch the error object...
The NSError object is immutable once created. Its data cannot be changed, only copied into or replaced with a separate error object. Several Cocoa classes also return NSError objects as part of their method calls. Use those error objects instead of creating your own.


