Mar 22 2012

An NSError Error

We have a lot of very convenient, very powerful methods at our disposal such as [NSData dataWithContentsOfFile:]. This method goes to the file system, opens the file, reads in all the bytes, closes the file, packs the bytes into an NSData, and returns it back to us. It replaces a loop and several other lines of code into one convenient package. If it can’t do the work, it returns nil. That’s pretty simple.

There’s just one problem. It’s great that you’re told that things didn’t work, but you have no idea why it failed. File doesn’t exist? Permissions problem? File too big to fit into memory? You don’t know, so you might write code like this:

NSData *data = [NSData dataWithContentsOfFile: @"/usr/share/dict/words"];
if (data == nil) {
    // uh, tell the user something.  Wish we knew more details.
}

An alternative is to write your own open / read-loop / pack / close code so you know exactly what went wrong, so you can tell the user.

With Mac OS X 10.5 Apple started introducing methods that return error information via a pointer you pass in:

NSError *error;
NSData *data = [NSData dataWithContentsOfFile: @"/usr/share/dict/words"
                       options: 0
                       error: &error];
if (data == nil) {
    // Tell the user exactly what happened by digging 
    // into the error object.
}

You can think of the error details as out-of-band data, a secondary information stream coming from the method. “Here are the bytes you asked for. Oh, and if something went wrong, you can look over here for more information.” You give a pointer to an NSError pointer (which ends up being an NSError **) which gets filled in with an (autoreleased) NSError if something goes wrong.

I do a lot of code review for friends and coworkers, and I’ve seen a common mistake, even with experienced developers: using the value of the error pointer, rather than the return value of the method, to decide whether the method succeeded or failed

This is bad code:

NSError *error;
NSData *data = [NSData dataWithContentsOfFile: @"/usr/share/dict/words"
                       options: 0
                       error: &error];
if (error != nil) {
    // Please don't use the error pointer to decide method success or failure.
    // Compare |data| to nil first
}

You should always look at the return code of the method. In the case of dataWithContentsOfFile:..., you should test to see if data is nil, and only if it is nil would you use the error pointer for more elaboration. On success, the method being called is under no obligation to fill in error with a sane value. It could fill it with an insane value. It could even assign a temporary object to it and not reset it to NULL. So you cannot trust the value of the returned error pointer unless the method told you to.

Apple’s rules are pretty simple: the error pointer is hands-off unless the method indicates a failure. Trying to do otherwise could invite odd behavior, crashes, or at the very least your coworkers saying “neener neener neener” when they see the code.

Interested in error handling? Advanced Mac OS X Programming: The Big Nerd Ranch Guide devotes an entire chapter to the topic.

3 Comments

  1. Alan Zeino

    What guarantees does Apple make about the pointer passed in if the returned object is nil? Will it _always_ have a filled in value if the result is nil? Or should I be a little more careful and do this:

    NSError *error;
    id thing = [something method:foo error: &error];
    if (thing == nil)
    {
    if (error != nil)
    {
    // use error
    }
    }

  2. Mark Dalrymple

    Hi Alan,

    The docs are usually pretty clear about what gets filled in: e.g. for NSData’s dataWithContentsOfFile:options:error: “If an error occurs, upon return contains an NSError object that describes the problem”.

    Even if it is nil, don’t forget that message sends to nil are no-ops in Objective-C. Integer return types (including pointers and BOOL) and floats will return zero values. Returned structs you can’t really trust their value.

  3. Mike Abdullah

    If there’s an failure, but the error pointer isn’t filled in, that’s a major mistake on the framework’s part. It results in a crash on your end, and should be reported to Apple. I’ve only ever come across this problem in 10.7.3′s security-scoped bookmarks API, so it is highly rare.

    The called method is entitled to fill in the error pointer with nil, as a kind of “er, I dunno what went wrong” gesture. There are odd parts of Cocoa which do this. Again, Apple really ought to do a better job and supply a proper error. If you pass such an error to -presentError: or similar, you’ll find a snarky message in the console saying that the user deserves better!

    Finally, I’d like to note that this pattern precedes Mac OS X 10.5. The doc architecture/Core Data definitely started using it in 10.4, and NSError itself was introduced with WebKit back in the 10.2 days.

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>