Using NSError to Increase Robustness & Useability

×

This post was originally published in 2011
It may contain stale & outdated information. Or it may have grown more awesome with age, like the author.

It wasn’t until we (long ago) decided to use the hardcore ‘E_ALL‘ error reporting level in our development environment that I started to realize the value of being overly paranoid when coding. Long ago I recall I did this, but I think PHP made me a little lazy – it’s a very forgiving language, so long as one uses an appropriate error reporting level ;)

Many Cocoa objects implement methods that take an optional error parameter. Years ago, before I was forced to take a long sabbatical from Cocoa programming, I interpreted ‘optional’ as ‘ignore’.

When I had time to get back into Cocoa, I tasked myself with tidying up Slider 2′s code-base. I found myself cringing in a few places, noticing possible situations that could cause Rapid Weaver to … crash and die.

Enter NSError.

To highlight it’s utility, and how it can enhance both useability and the robustness of your Cocoa application / plugin, I provide the following example. Consider a method that should read some data from a file (with a specific format) then unarchive the data. This is precisely what Slider 2 must do when one attempts to load a Slidebrary item from a file. We need to ensure that the file has the correct extension, the data can be read, and that it can be unarchived. We need to do this without causing Rapid Weaver to crash when either of the aforementioned three expectations aren’t realized. The caller also needs to know what went wrong, so the user can be informed.

The method that accomplishes this is reproduced below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
+ (SSlidebraryItem *) loadFromPath:(NSString *)path error:(NSError **)error {
    // 1) check the path - if it's incorrect create an NSError object and populate it.
    if (![[path pathExtension] isEqualToString:[SSlidebraryItem fileExtension]]) {
        // Unacceptable extension!
        // Populate our error variable
        // I've used a convention outlined here: http://stackoverflow.com/a/3276356/187954
        // Notice that userInfo is human-readable. This is the string that we can use to inform our user of the error
        *error = [NSError errorWithDomain:@"com.codeofinterest.Slider2" 
                                     code:100 
                                 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Passed an invalid file format \"%@\" at \"%@\"", [path pathExtension], path]  forKey:NSLocalizedDescriptionKey]];
        return nil; // Caller should be checking that the error object is nil, and reacting appropriately
    }
 
    // File extension was acceptable - attempt to read the data from the file
    // We pass the error object to -initWithContentsOfFile
    NSData *encodedSettings = [[NSData alloc] initWithContentsOfFile:path options:NSDataReadingUncached error:error];
 
    // If something went wrong, return nil. 
    // The error object will have been populated by -initWithContentsOfFile
    if (*error != nil) {
        return nil;
    }
 
   // Have to use a try ... catch block here because +unarchiveObjectWithData raises an      
   // NSInvalidArchiveOperationException if data is not a valid archive, instead of returning nil and 
   // populating an error object
    @try {
 
        SSlidebraryItem *loadedSlidebraryItem = [NSKeyedUnarchiver unarchiveObjectWithData:encodedSettings];
 
        // No data! This shouldn't happen... 
        if (!loadedSlidebraryItem) {
            // Populate the error object with details of what happened
            *error = [NSError errorWithDomain:@"com.codeofinterest.Slider2" code:101 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Failed to load slidebrary item from \"%@\"", path] forKey:NSLocalizedDescriptionKey]];
            return nil;
        }
        // Everything went well, return the loaded, unarchived object!
        return loadedSlidebraryItem;
    }
    @catch (NSException *exception) {
        // An exception occurred! This calls for another error message.
        *error = [NSError errorWithDomain:@"com.codeofinterest.Slider2" code:102 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Failed to unarchive slidebrary item \"%@\"", path] forKey:NSLocalizedDescriptionKey]]; 
        // Also log the exception, so if a user emails you asking why their Slidebrary item won't load, you can 
        // ask them to run Rapid Weaver in a terminal and copy - paste any error output to aid your debugging!
        // I like to use the logging extension described here: http://pagesofinterest.net/blog/2011/09/more-informative-nslog-alternative/
        SLog(@"%@", exception);
        return nil;
    }
}

As an example of a possible way to handle functions that provide error information in this manner, the loadSelectedSlidebraryItem method is included below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (SSlidebraryItem *) loadSelectedSlidebraryItem {
    // Prepare the path for the Slidebrary item the user has selected
    NSDictionary *selectedSlidebraryItem = [[slidersController arrangedObjects] objectAtIndex:[table selectedRow]];
    NSString *path = [[SSlidebraryController slidebraryDirectory] stringByAppendingPathComponent:[selectedSlidebraryItem objectForKey:key_si_filename]];
 
    NSError *loadError = nil; // Prepare our error object
 
    SSlidebraryItem *settings = [SSlidebraryItem loadFromPath:path error:&loadError];
 
    // There was a problem loading the file
    if (loadError != nil) {
        // I like to slide a modal dialog out informing the user of the error as 
        // it makes it impossible to ignore and  easy for them to copy-paste or
        // grab a screenshot & email me the details
 
        [SModalSheetDialogs showModalMessage:@"An error occurred" 
                         withInformativeText:[loadError localizedDescription] 
                                   withStyle:NSWarningAlertStyle
                                  fromSender:self];
        return nil;
    } 
 
    // Success - return the loaded object
    return settings;
}

Summary: using NSError within your application / plugin is more work, but makes for a more robust and user-friendly product. Two facets of a project that should sit high on the priority list of every developer.

Comments (2) | Trackback