The beauty of NSError
Brent Simmons in Init Question:Let’s say you have an object where initWithSomething could fail due to bad inputs or other error.
Let’s also say that, if it fails, an error should probably be presented to the user.
Brent lists a number of different options. In Programming with Objective-C, there is a golden rule to error handling in ObjC:
- If it is a programmer error, use exceptions/assertions.
- If it is any other kind of error, use NSError.
This is yet another one of those genius API design decisions that I just love with ObjC. It’s consistent, sane and available everywhere. It leaves very little room for bikeshedding, while still being very convenient to work with most circumstances.
This also eliminates option 4, as it uses assertions for a user/environment error, which would leave your app unusable without any obvious way of fixing it if anything is even slightly wrong. Danger! Don’t do that!
Of the remaining alternatives, I would definitely opt for #1: a failable constructor with in-out NSError parameter. There’s tons of precedent all over Foundation/AppKit/UIKit for this pattern, e g:
-[NSData initWithContentsOfURL:options:error:]
-[NSString initWithContentsOfURL:encoding:error:]
-[NSXMLDocument initWithXMLString:options:error:]
Displaying errors
A very “convenient” option is to just display errors where they happen, and not propagate them. This breaks MVC, and means that your model will likely be too coupled to how the data is displayed, and makes it hard to test and reuse. Make sure to propagate errors up to your controller layer before they’re displayed; either through direct calls and in-out parameters, or delegate protocols. I often have protocols with almost only an “I failed with this error” method.
So, if you’re displaying errors so far from their source, how do you handle them and gracefully get back to a working state, if that working state depends on user interaction? NSError actually solved this problem rather beautifully back on Mac OS, but the whole infrastructure for this wasn’t ported down to iOS: only some bits of pieces followed along. Use these!
You have probably used NSLocalizedDescriptionKey
when you’ve constructed your own NSErrors, which is like a ‘subject line’ for the error. There are many more keys you can use to customize errors! There is also NSLocalizedFailureReasonErrorKey
, which is like the 'message body’ of the error. NSLocalizedRecoverySuggestionErrorKey
lets you tell your user how they could resolve this invonvenient situation. NSUnderlyingErrorKey
is very interesting for other programmers: telling them what caused this error in the first place, if that’s available.
But it gets better! The key NSLocalizedRecoveryOptionsErrorKey
lets you provide an NSArray of named options for how to solve the error. These are like the button titles of a dialog. You couple it with NSRecoveryAttempterErrorKey
, which is any object that conforms to the informal protocol NSErrorRecoveryAttempting
.
Hey, this sounds like you could write some generic code that just displays NSError to a user? With title, subject, buttons and actions. Yeah! This is actually what us old Mac-heads had with -[NSApplication presentError::::]
. No such equivalent exists in UIKit, but it is easy to build on your own.
LookbackErrorPresenter is an error presenter for iOS, like the one on built into AppKit on Macs, which I just open sourced for your pleasure. You just give it a well-formed NSError with any or all of the above options, and it will be presented and logged appropriately. Here’s some sample usage, straight from the Lookback sources:
NSURL *url = [_group remoteGroupURL] ?: [NSURL URLWithString:_group.endpointURLString];
LookbackRecoveryAttempter *recovery = [[LookbackRecoveryAttempter alloc] initWithNamesAndCallbacks:
@"Manage experience", ^BOOL{
GFOpenURL(url);
return YES;
},
@"Dismiss", ^BOOL{ return NO; },
nil, nil
];
[[LookbackErrorPresenter presenter] presentError:[NSError errorWithDomain:@"io.lookback" code:-18300 userInfo:@{
NSLocalizedDescriptionKey: @"Experience already published",
NSLocalizedFailureReasonErrorKey: @"You can delete or manage this experience via the Lookback website.",
NSRecoveryAttempterErrorKey: recovery,
NSLocalizedRecoveryOptionsErrorKey: recovery.recoveryOptions,
}] completion:nil];
My approach in controller level code is to create user-friendly NSErrors, populated with ALL the keys above and a recovery attempter, and just display it straight to the user and let them choose how to resolve the error. I can then also include the underlying error, to let more technical-savvy users understand the technical cause, and as a help to future programmers.
Error handling is very important, and if you have the right toolbox, not particularly hard! Just be vigilant, never ignore an error parameter, and propagate errors to where it can be correctly handled.
Additional resources
- RealMac on Cocoa Error Handling and Recovery
- My own Three points on error handling