NSError
The Foundation Framework NSError class is used to create error objects. The properties of this class are an error code, the error domain, and a dictionary of user information. The class includes methods for creating and initializing error objects, retrieving error properties, getting a localized error description, and facilitating error recovery.
An error domain is a mechanism used to organize error codes according to a system, subsystem, framework, and so forth. Error domains enable you to identify the subsystem, framework, and so forth, that detected the error. They also help prevent naming collisions between error codes, because error codes between different domains can have the same value. The user info dictionary is anNSDictionary instance that holds error information beyond the code and domain. The types of information that can be stored in this dictionary include localized error information and references to supporting objects. The following statement creates an error object using the NSError class factory method errorWithDomain:code:userInfo:.
NSError *err = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFileNoSuchFileError
userInfo:nil];
This particular error would be created if, for example, a file is not found at the path specified. Notice that the user info dictionary is not provided (the userInfo parameter is set to nil). Listing 14-1creates the same error object, this time with a user info dictionary.
Listing 14-1. Creating an NSError Object
NSString *desc = NSLocalizedString(@"FileNotFound", @"");
NSDictionary *info = @{NSLocalizedDescriptionKey:desc};
NSError *err = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFileNoSuchFileError
userInfo:info];
Listing 14-1 shows the user info dictionary for this example consists of one entry, the key-value pair for a localized description. The localized string is created using the Foundation NSLocalizedStringfunction. The constant NSLocalizedDescriptionKey is a standard user info dictionary key defined in the NSError class.
The Foundation Framework declares four major error domains:
- NSMachErrorDomain: OS kernel error codes.
- NSPOSIXErrorDomain: Error codes derived from standard POSIX-conforming versions of Unix, such as BSD.
- NSOSStatusErrorDomain: Error codes specific to Apple OS X Core Services and the Carbon framework.
- NSCocoaErrorDomain: All of the error codes for the Cocoa frameworks (this includes the Foundation Framework and other Objective-C frameworks).
In addition to the major error domains presented here, there are also error domains for frameworks, groups of classes, and even individual classes. The NSError class also enables you to create your own error domain when creating and initializing an NSError object. As mentioned previously, Listing 14-1uses the constant NSLocalizedDescriptionKey. The NSError class defines a set of common user info dictionary keys that can be used to create the key-value pairs for the user info dictionary. These keys are listed in Table 14-1.
Table 14-1. NSError Standard User Info Dictionary Keys
Key | Value Description |
---|---|
NSLocalizedDescriptionKey | Localized string representation of the error. |
NSFilePathErrorKey | The file path of the error. |
NSStringEncodingErrorKey | An NSNumber object containing the string encoding value. |
NSUnderlyingErrorKey | The error encountered in an underlying implementation (which caused this error). |
NSURLErroKey | An NSURL object. |
NSLocalizedFailureReasonErroryKey | Localized string representation of the reason that caused the error. |
NSLocalizedRecoverySuggestionErrorKey | Localized recovery suggestion for the error. |
NSLocalizedRecoveryOptionsErrorKey | NSArray containing the localized titles of buttons for display in an alert panel. |
NSRecoveryAttempterErrorKey | An object that conforms to theNSErrorRecoveryAttempting protocol. |
NSHelpAnchorErrorKey | Localized string representation of help information for a help button. |
NSURLErrorFailingURLString | NSURL object containing the URL that caused the load to fail. |
NSURLErrorFailingURLStringErrorKey | String for the URL that caused the load to fail. |
NSURLErrorFailingURLPeerTrustErrorKey | SecTrustRef object representing the state of a failed SSL handshake. |
Using Error Objects
OK, so now you know how to create NSError objects, but how do you use them? In general, there are two scenarios for obtaining NSError objects:
- Delegation: An error object passed as a parameter to a delegation method that you implement.
- Indirection: An error object retrieved via indirection from a method that your code invokes.
The delegation pattern is a design pattern whereby an object (the delegator) delegates one or more tasks to another delegate object. The tasks are encapsulated in the method(s) of the delegate object. When necessary, the delegator invokes the appropriate method on the delegate object, providing any required parameters. Many Foundation Framework classes implement the delegation pattern to enable custom error handling. The delegating Foundation object invokes a method on a delegate object (custom code that you implement) that includes an error object as a parameter.
The NSURLConnectionDelegate protocol declares a delegation method (connection:didFailWithError:) that returns an error object:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
This protocol is used for asynchronously loading a URL request via an NSURLConnection object. Your code implements a delegate object that conforms to this protocol and sets it as the delegate of theNSURLConnection object. Your code then loads the URL request asynchronously, and if an error occurs, the delegating (NSURLConnection) object invokes the connection:didFailWithError:method on your delegate object.
A common Objective-C programming convention for methods that return an error object is to make this the last parameter of the method and to specify the type of this parameter as a pointer to an error object pointer (also known as double-indirection). The following example declares a method named getGreeting that has an error object of type NSError as its parameter.
- (NSString *)getGreeting(NSError **error);
This approach enables the called method to modify the pointer the error object points to, and if an error occurs, return an error object specific to the method call.
A return value for the method is required. It is either an object pointer or a Boolean value. Your code invokes such a method by including a reference to an error object as its last parameter, or providingNULL as the last parameter if you don’t need to access the error. After the method is invoked, the result is inspected. If the value is NO or nil, then the error object should be processed; else no error object was returned. Many Foundation classes have methods that return an error object by indirection. In addition, your classes can use this convention to implement methods that return an error object.
Listing 14-2 depicts a class named FileWriter, which declares a method that (indirectly) returns an error object.
Listing 14-2. FileWriter Interface with Method That Returns an NSError Object
@interface FileWriter : NSObject
+ (BOOL) writeData:(NSData *)data toFile:(NSString *)path error:(NSError **)err;
@end
For your code to call this method on a FileWriter object, it must first declare an NSError object, as shown in Listing 14-3, and then check the return value to see if an error occurred.
Listing 14-3. Invoking FileWriter Method That Returns an NSError Object
NSError *writeErr;
NSData *greeting = [@"Hello, World" dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [FileWriter writeData:greeting
toFile:NSTemporaryDirectory()
error:&writeErr];
if (!success)
{
// Process error
...
}
Now you’ll create a couple of example programs that perform error handling for error objects passed by delegation methods and error objects obtained via indirection.
Handling Delegation Method Errors
Now you’ll implement a program that performs error handling for a delegate method. In Chapter 11, you implemented a program, NetConnector, which demonstrates URL loading using the Foundation Framework NSURLConnection class. Here you’ll update this program to handle errors when loading a URL.
In Xcode, open the NetConnector project by selecting Open NetConnector.xcodeproj from the Xcode File menu. The source code for the project consists of three files that implement theNetConnector class and the main function. Let’s start by making some updates to the NetConnectorclass. Select the NetConnector.m file in the navigator pane, and then update the NetConnector implementation (updates in bold), as shown in Listing 14-4.
Listing 14-4. NetConnector Class Implementation, Updated to Handle Errors
#import "NetConnector.h"
@interface NetConnector()
@property NSURLRequest *request;
@property BOOL isFinished;
@property NSURLConnection *connector;
@property NSMutableData *receivedData;
@end
@implementation NetConnector
- (id) initWithRequest:(NSURLRequest *)request
{
if ((self = [super init]))
{
_request = request;
_finishedLoading = NO;
// Create URL cache with appropriate in-memory storage
NSURLCache *URLCache = [[NSURLCache alloc] init];
[URLCache setMemoryCapacity:CACHE_MEMORY_SIZE];
[NSURLCache setSharedURLCache:URLCache];
// Create connection and begin downloading data from resource
_connector = [NSURLConnection connectionWithRequest:request delegate:self];
}
return self;
}
- (void) reloadRequest
{
self.finishedLoading = NO;
self.connector = [NSURLConnection connectionWithRequest:self.request
delegate:self];
}
#pragma mark -
#pragma mark Delegate methods
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSString *description = [error localizedDescription];
NSString *domain = [error domain];
NSInteger code = [error code];
NSDictionary *info = [error userInfo];
NSURL *failedUrl = (NSURL *)[info objectForKey:NSURLErrorFailingURLErrorKey];
NSLog(@"\n*** ERROR ***\nDescription-> %@\nURL-> %@\nDomain-> %@\nCode-> %li",
description, failedUrl, domain, code);
self.finishedLoading = YES;
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
if (self.receivedData != nil)
{
[self.receivedData setLength:0];
}
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
NSURLResponse *response = [cachedResponse response];
NSURL *url = [response URL];
if ([[url scheme] isEqualTo:HTTP_SCHEME])
{
NSLog(@"Downloaded data, caching response");
return cachedResponse;
}
else
{
NSLog(@"Downloaded data, not caching response");
return nil;
}
}
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
if (self.receivedData != nil)
{
[self.receivedData appendData:data];
}
else
{
self.receivedData = [[NSMutableData alloc] initWithData:data];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSUInteger length = [self.receivedData length];
NSLog(@"Downloaded %lu bytes from request %@", length, self.request);
// Loaded data, set flag to exit run loop
self.finishedLoading = YES;
}
@end
An NSURLConnection sends the connection:didFailWithError: message to its delegate object if the connection doesn’t load its request successfully. As shown in Listing 14-4, the NetConnector class is set as the delegate for its NSURLConnection object, and hence its connection:didFailWithError: method will be invoked on request load errors. The method implementation retrieves the properties of the NSError object: description, domain, code, and user info. The URL of the failed load request is stored in the user info dictionary. Its key is NSURLErrorFailingURLErrorKey, one of the NSError standard user info dictionary keys listed in Table 14-1. The method logs the values of this data to the output pane.
Next, you will update the main() function, but before you do that, you need to create a page with a valid URL that cannot be loaded. Doing this forces an error to occur when you attempt to load the page using an NSURLConnection object. Open an OS X terminal window and enter the Unix commands shown in Listing 14-5.
Listing 14-5. Using the Mac OS X Terminal Utility to Create a File
touch /tmp/ProtectedPage.html
chmod u-r /tmp/ProtectedPage.html
The touch command creates a new, empty file. As shown in Listing 14-6, the full path for the file is/tmp/ProtectedPage.html. The chmod u-r command removes read access to the file for the current user. The corresponding URL for this resource is file:///tmp/ProtectedPage.html. OK, now that this resource is configured properly, let’s update the main() function. Select the main.m file in the navigator pane, and then update the main() function, as shown in Listing 14-6.
Listing 14-6. NetConnector main( ) Function Implementation
#import <Foundation/Foundation.h>
#import "NetConnector.h"
#define INDEX_URL @"file:///tmp/ProtectedPage.html"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
// Retrieve the current run loop for the connection
NSRunLoop *loop = [NSRunLoop currentRunLoop];
// Create the request with specified cache policy, then begin downloading!
NSURLRequest *request = [NSURLRequest
requestWithURL:[NSURL URLWithString:INDEX_URL]
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:5];
NetConnector *netConnect = [[NetConnector alloc] initWithRequest:request];
// Loop until finished loading the resource
while (!netConnect.finishedLoading &&
[loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
// Zero out cache
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
return 0;
}
The key change in the main() function is the URL. It is updated to the URL for the resource that you created earlier, file:///tmp/ProtectedPage.html. The code will attempt to asynchronously load this resource (with an NSURLConnection object) using the NetConnector initWithRequest: method. As shown in Listing 14-4, the NetConnector class implements the connection:didFailWithError:method to handle errors loading a URL.
Now save, compile, and run the updated NetConnector program and observe the messages in the output pane (as shown in Figure 14-1).
Figure 14-1. Testing the NSError portion of the updated NetConnector project
The messages in the output pane show that the NSURLConnection failed to load the URL, and hence sent the connection:didFailWithError: message to its delegate object—in this case, theNetConnector instance. As shown in the method implementation of Listing 14-4, the error description, failed URL, error code, and domain are logged to the output pane. OK, great. Now that you’ve got that under your belt, let’s implement a program that returns an error object via indirection.
Creating Errors Objects via Indirection
Now you will create a program that demonstrates error handling for a Foundation Framework object. In Xcode, create a new project by selecting New Project . . . from the Xcode File menu. In the New Project Assistant pane, create a command-line application. In the Project Options window, specifyFMErrorObject for the Product Name, choose Foundation for the Project Type, and select ARC memory management by selecting the Use Automatic Reference Counting check box. Specify the location in your file system where you want the project to be created (if necessary select New Folderand enter the name and location for the folder), uncheck the Source Control check box, and then click the Create button.
In the Xcode project navigator, select the main.m file and update the main() function, as shown inListing 14-7.
Listing 14-7. FMErrorObject main( ) Function Implementation
#import <Foundation/Foundation.h>
#define FILE_PATH @"/tmp/NoSuchFile.txt"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
NSFileManager *fileMgr = [NSFileManager defaultManager];
NSError *fileErr;
BOOL success = [fileMgr removeItemAtPath:FILE_PATH error:&fileErr];
if (!success)
{
NSString *description = [fileErr localizedDescription];
NSString *domain = [fileErr domain];
NSInteger code = [fileErr code];
NSDictionary *info = [fileErr userInfo];
NSURL *failedPath = (NSURL *)[info objectForKey:NSFilePathErrorKey];
NSLog(@"\n*** ERROR ***\nDescription-> %@\nPath-> %@\nDomain-> %@\nCode-> %li",
description, failedPath, domain, code);
}
}
return 0;
}
Logically, the code uses an NSFileManager instance method to remove a file from the file system. If an error occurs when invoking the method, it returns an error object via indirection that describes the error, along with an appropriate return result. As shown in Listing 14-7, the file begins by defining a variable, FILE_PATH, which represents the full path of a file on the local file system (in order to test error object creation and processing make sure that this file, /tmp/NoSuchFile.txt, doesn’t exist). The main() function begins by creating a FileManager object and an NSError pointer. It then attempts to delete the file by invoking the NSFileManager removeItemAtPath:error: method. As the error parameter is not NULL, an NSError object will be returned if an error occurs when invoking this method.
Next, a conditional expression is performed using the Boolean result of this method; if the returned value is NO, then an error occurred trying to remove the file and the body of the conditional expression is executed. This code retrieves the properties of the NSError object: description, domain, code, and user info. The full path of the file that couldn’t be removed is stored in the user info dictionary. Its key is NSFilePathErrorKey, one of the NSError standard user info dictionary keys listed in Table 14-1. The method logs the values of this data to the output pane.
Now save, compile, and run the FMErrorObject program and observe the messages in the output pane (as shown in Figure 14-2).
Figure 14-2. Testing NSError by indirection in the FMErrorObject project
The messages in the output pane show that the NSFileManager object failed to remove the file, and hence set an error object (via indirection) in the error parameter of the removeFileWithPath:error:method, and set its return result to NO. The error description, file path, error code, and domain are logged to the output pane. Perfect. Now that you understand how to retrieve and process error objects, let’s examine the Foundation Framework support for error recovery.
Error Recovery
The NSError class provides a mechanism for recovering from errors. TheNSErrorRecoveryAttempting informal protocol provides methods that are implemented to perform error recovery. An object that adopts this protocol must implement at least one of its two methods for attempting recovery from errors. The user info dictionary of an NSError object that supports error recovery must contain, at a minimum, the following three entries:
- The recovery attempter object (retrieved using the keyNSRecoveryAttempterErrorKey)
- The recovery options (retrieved using the keyNSLocalizedRecoveryOptionsErrorKey)
- A localized recovery suggestion string (retrieved using the keyNSLocalizedRecoverySuggestionErrorKey)
The recovery attempter may implement any logic appropriate for error recovery. Note that the error recovery functionality is only available on the Apple OS X platform.
Error Responders
The Application Kit provides APIs and mechanisms that can be used to respond to errors encapsulated in NSError objects. The NSResponder class defines an error responder chain used to pass events and action messages up the view hierarchy. It includes methods to display information in the associatedNSError object, and then forwards the error message to the next responder. This enables each object in the hierarchy to handle the error appropriately, perhaps by adding additional information pertaining to the error. Note that the error responder functionality is available only on the Apple OS X platform.