Recovering From Errors

As described in “The Recovery Attempter,” an NSError object can have a designated recovery attempter, an object that attempts to recover from the error if the user requests it. The error object holds a reference to the recovery attempter in its user info dictionary, so if the error object is passed around within an application, the recovery attempter stays with it. The user info dictionary must also contain recovery options, an array of localized strings for button titles, one or more of which requests recovery. When the error is presented in an alert and the user selects the recovery option, a message is sent to the recovery attempter, requesting it to do its job.

Ideally, the recovery attempter should be an independent object that knows something about the conditions of an error and how best to circumvent those conditions. An application could even have an object whose role is to recover from errors of various kinds. A recovery attempter must implement at least one of the two methods of the NSErrorRecoveryAttempting informal protocol: attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo: or attemptRecoveryFromError:optionIndex:. It implements the former method for error alerts presented document-modally, and the latter method for application-modal alerts.

You must also prepare an error object so that error recovery can take place. To do this, add three items to the user info dictionary of the error object:

Listing 5-1 illustrates a case involving the NSXMLDocument class. In this example, an NSDocument object attempts to create an internal tree representing an XML document using the initWithContentsOfURL:options:error: method of NSXMLDocument. If the attempt fails, the usual cause is that the source XML is malformed—for example, there is a missing end tag for an element, or an attribute value is not quoted. If the source XML is “tidied” first to fix the structural problems, it may be possible to create the XML tree.

In the example in Listing 5-1 if the invocation of initWithContentsOfURL:options:error: returns an error object by reference, the document object customizes the error object, adding (among other things) a recovery-attempter object, localized recovery options, and a localized recovery suggestion to its user info dictionary. Then it sends presentError:modalForWindow:delegate:didPresentSelector:contextInfo: to self.

Listing 5-1  Preparing for error recovery

- (BOOL)readFromURL:(NSURL *)furl ofType:(NSString *)type error:(NSError **)anError {
    NSError *err;
 
    // xmlDoc is an NSXMLDocument instance variable.
    if (xmlDoc != nil) {
        xmlDoc = nil;
    }
 
    xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:furl
            options:NSXMLNodeOptionsNone error:&err];
 
    if (xmlDoc == nil && err) {
        NSString *newDesc = [[err localizedDescription] stringByAppendingString:
            ([err localizedFailureReason] ? [err localizedFailureReason] : @"")];
 
        NSDictionary *newDict = @{ NSLocalizedDescriptionKey : newDesc,
            NSURLErrorKey : furl,
            NSRecoveryAttempterErrorKey : self,
            NSLocalizedRecoverySuggestionErrorKey :
                NSLocalizedString(@"Would you like to tidy the XML and try again?", @""),
            NSLocalizedRecoveryOptionsErrorKey :
                @[NSLocalizedString(@"Try Again", @""), NSLocalizedString(@"Cancel", @"")] };
 
        NSError *newError = [[NSError alloc] initWithDomain:[err domain]
            code:[err code] userInfo:newDict];
        [self presentError:newError modalForWindow:[self windowForSheet]
            delegate:self
            didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:)
            contextInfo:nil];
    }
// ...

Note that the document object also adds to the user info dictionary the URL identifying the source of XML. The recovery attempter will use this URL when it attempts to create a tree representing the XML.

The error object is passed up the error-responder chain and NSApp displays it. When the user clicks any button of the error alert, NSApp checks to see if the error object has both a recovery attempter and recovery options. If both of these conditions are true, it invokes the method implemented by the recovery attempter that corresponds to the mode of the alert (that is, document-modal or application-modal).

Listing 5-2 shows how the recovery attempter for the XML document implements the attemptRecoveryFromError:optionIndex:delegate:didRecoverSelector:contextInfo: method.

Listing 5-2  Recovering from the error and informing the modal delegate

- (void)attemptRecoveryFromError:(NSError *)error
                     optionIndex:(unsigned int)recoveryOptionIndex
                        delegate:(id)delegate
              didRecoverSelector:(SEL)didRecoverSelector
                     contextInfo:(void *)contextInfo {
 
    BOOL success = NO;
    NSError *err;
    NSInvocation *invoke = [NSInvocation invocationWithMethodSignature:
                               [delegate methodSignatureForSelector:didRecoverSelector]];
    [invoke setSelector:didRecoverSelector];
 
    if (recoveryOptionIndex == 0) { // Recovery requested.
        xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:[[error userInfo]
                objectForKey:NSURLErrorKey] options:NSXMLDocumentTidyXML error:&err];
        if (xmlDoc != nil) {
            success = YES;
        }
    }
    [invoke setArgument:(void *)&success atIndex:2];
    if (err)
        [invoke setArgument:&err atIndex:3];
    [invoke invokeWithTarget:delegate];
}

The key part of the above example is the test the recovery attempter makes to determine if the user clicked the “Try Again” button: it checks the value of recoveryOptionIndex. If the user did click this button, the recovery attempter invokes the initWithContentsOfURL:options:error: method again, this time with the NSXMLDocumentTidyXML option. Then it creates and invokes an NSInvocation object, thereby sending the required message to the modal delegate of the error alert. The invocation object includes the two parameters required by the delegate’s selector: a Boolean indicating whether the recovery attempt succeeded and a “context info” parameter which, in this case, contains any error object returned from the recovery attempt.

When the modal delegate receives the message from the recovery attempter, as in Listing 5-3, it can respond appropriately.

Listing 5-3  Modal delegate responding to recovery attempter

- (void)didPresentErrorWithRecovery:(BOOL)didRecover
            contextInfo:(void *)contextInfo {
 
    NSError *theError = (NSError *)contextInfo;
    if (didRecover) {
        [tableView reloadData];
    } else if (theError && [theError isKindOfClass:[NSError class]]) {
        [NSAlert alertWithError:theError];
    }
}