Chapter 10: Tackling Those Pesky Errors

Error management can be one of the most frustrating parts of development. It’s hard enough getting everything to work when things go well, but to build really great apps, you need to manage things gracefully when they go wrong. Cocoa provides some tools to make the job easier.

In this chapter, you find the major patterns that Cocoa uses to handle errors that you should use in your own projects. You also discover the major error-handling tools, including assertions, exceptions, and NSError objects. Because your program may crash in the field, you learn how to get those crash reports from your users, and how to log effectively and efficiently.

Error-Handling Patterns

There are several useful approaches to handling errors. The first and most obvious is to crash. This isn’t a great solution, but don’t discount it too quickly. I’ve seen a lot of very elaborate code around handling extremely unlikely errors, or errors you won’t be able to recover from anyway. The most common of these is failure to allocate memory. Consider the following code:

  NSString *string = [NSString stringWithFormat:@”%d”, 1];

  NSArray *array = [NSArray arrayWithObject:string];

It’s conceivable (not really, but let’s pretend) that stringWithFormat: might fail because Foundation isn’t able to allocate memory. In that case, it returns nil, and the call to arrayWithObject: throws an exception for trying to insert nil into an array, and your app probably crashes. You could (and in C you often would) include a check here to make sure that doesn’t happen. Don’t do that. Doing so needlessly complicates the code, and there’s nothing you’re going to be able to do anyway. If you can’t allocate small amounts of memory, the OS is very likely about to shut you down anyway. Besides, it’s almost impossible to write error-handling code in Objective-C that does not itself allocate memory. Accept that in this impossible case you may crash, and keep the code simple.

The next closely related error-handling approach is NSAssert. This raises an NSInternalInconsistencyException, which by default crashes your program. Particularly during development, this is a very good thing. It “fails fast,” which means the failure tends to happen close to the bug. One of the worst things I see in code is something like this:

// Do not do this

- (void)doSomething:(NSUInteger)index {

  if (index > self.maxIndex) {

    return;

  }

  ...  

}

Passing an out-of-range index is a programming error. This code swallows that error, turning it into a no-op. That is incredibly difficult to debug. Note how NSArray handles this situation. If you pass an index out of range, it raises an exception very similar to NSAssert. It’s the caller’s job to pass good values. The worst thing NSArray could do is to silently ignore bad values. It’s better to crash. I’ll discuss assertions more in the following two sections, “Assertions” and “Exceptions,” including how to manage development and release builds, and how to make these a bit more graceful.

The lesson here is that crashing is not the worst-possible outcome. Data corruption is generally the worst-possible outcome, and if getting into a deeply unknown state could corrupt user data, it’s definitely better to crash.

Expected errors should be handled gracefully and should never crash. The common pattern for managing expected errors is to return an NSError object by reference. I discuss this in the section “Errors and NSError” later in this chapter.

There is a major difference between expected and unexpected errors. In iOS, failure to allocate small amounts of memory is an unexpected error. It should never happen in normal operation. You should have received a memory warning and been terminated long before you got to that state. You can generally ignore truly unexpected errors and let them crash you. On the other hand, running out of disk space is a rare but expected error. It can easily happen if the user has requested that iTunes fill the device with music. You need to recover gracefully when you cannot write a file.

In the middle are programming errors. You generally handle these errors with assertions.

Assertions

Assertions are an important defense against programming errors. An assertion requires that something must be true at a certain point in the program. If it is not true, then the program is in an undefined state and should not proceed. Consider the following example of NSAssert:

NSAssert(x == 4, @”x must be four”);

NSAssert tests a condition, and if it returns NO, raises an exception, which is processed by the current exception handler, which by default calls abort and crashes the program. If you’re familiar with Mac development, you may be used to exceptions terminating only the current run loop, but iOS calls abort by default, which terminates the program no matter what thread it runs on.

Technically, abort sends the process a SIGABRT, which can be caught by a signal handler. Generally, we don’t recommend catching SIGABRT except as part of a crash reporter. See “Catching and Reporting Crashes,” later in this chapter, for information about how to handle crashes.

You can disable NSAssert by setting NS_BLOCK_ASSERTIONS in the build setting “Preprocessor Macros” (GCC_PREPROCESSOR_DEFINITIONS). Opinions differ on whether NSAssert should be disabled in release code. It really comes down to this: When your program is in an illegal state, would you rather it stop running, or would you prefer that it run in a possibly random way? Different people come to different conclusions here. My opinion is that it’s generally better to disable assertions in release code. I’ve seen too many cases where the programming error would have caused only a minor problem, but the assertion causes a crash. Xcode 4 templates automatically disable assertions when you build for the Release configuration.

That said, although I like removing assertions in the Release configuration, I don’t like ignoring them. They’re exactly the kind of “this should never happen” error condition that you’d want to find in your logs. Setting NS_BLOCK_ASSERTIONS completely eliminates them from the code. My solution is to wrap assertions so that they log in all cases. The following code assumes you have an RNLogBug function that logs to your log file. It’s mapped to NSLog as an example. Generally I don’t like to use #define, but it’s necessary here because __FILE__ and __LINE__ need to be evaluated at the point of the original caller.

This also defines RNCAssert as a wrapper around NSCAssert and a helper function called RNAbstract. NSCAssert is required when using assertions within C functions, rather than Objective-C methods.

RNAssert.h

#import <Foundation/Foundation.h>

#define RNLogBug NSLog // Use DDLogError if you’re using Lumberjack

// RNAssert and RNCAssert work exactly like NSAssert and NSCAssert

// except they log, even in release mode

#define RNAssert(condition, desc, ...)

  if (!(condition)) {

    RNLogBug((desc), ## __VA_ARGS__);

    NSAssert((condition), (desc), ## __VA_ARGS__);

  }

#define RNCAssert(condition, desc)

  if (!(condition)) {

    RNLogBug((desc), ## __VA_ARGS__);

    NSCAssert((condition), (desc), ## __VA_ARGS__);

  }

Assertions often precede code that would crash if the assertion were not valid. For example (assuming you’re using RNAssert to log even in the Release configuration):

RNAssert(foo != nil, @”foo must not be nil”);

[array addObject:foo];

The problem is that this still crashes, even with assertions turned off. What was the point of turning off assertions if you’re going to crash anyway in many cases? That leads to code like this:

RNAssert(foo != nil, @”foo must not be nil”);

if (foo != nil) {

  [array addObject:foo];

}

That’s a little better, using RNAssert so that you log, but you’ve got duplicated code. This raises more opportunities for bugs if the assertion and conditional don’t match. Instead, I recommend this pattern when you want an assertion:

if (foo != nil) {

  [array addObject:foo];

}

else {

  RNAssert(NO, @”foo must not be nil”);

}

This pattern ensures that the assertion always matches the conditional. Sometimes assertions are overkill, but this is a good pattern in cases where you want one. I almost always recommend an assertion as the default case of a switch statement, however.

switch (foo) {

  case kFooOptionOne:

    ...

    break;

  case kFooOptionTwo:

    ...

    break;

  default:

    RNAssert(NO, @”Unexpected value for foo: %d”, foo):

    break;

}

This way, if you add a new enumeration item, it will help you catch any switch blocks that you failed to update.

Exceptions

Exceptions are not a normal way of handling errors in Objective-C. From Exception Programming Topics (developer.apple.com):

The Cocoa frameworks are generally not exception-safe. The general pattern is that exceptions are reserved for programmer error only, and the program catching such an exception should quit soon afterwards.

In short, exceptions are not for handling recoverable errors in Objective-C. Exceptions are for handling those things that should never happen and which should terminate the program. This is similar to NSAssert, and in fact, NSAssert is implemented as an exception.

Objective-C has language-level support for exceptions using directives such as @throw and @catch, but you generally should not use these. There is seldom a good reason to catch exceptions except at the top level of your program, which is done for you with the global exception handler. If you want to raise an exception to indicate a programming error, it’s best to use NSAssert to raise an NSInternalInconsistencyException, or create and raise your own NSException object. You can build these by hand, but we recommend +raise:format: for simplicity.

[NSException raise:NSRangeException

            format:@”Index (%d) out of range (%d...%d)”,

              index, min, max];

There seldom is much reason to do this. In almost all cases, it would be just as clear and useful to use NSAssert. Because you generally shouldn’t catch exceptions directly, the difference between NSInternalInconsistencyException and NSRangeException is rarely useful.

Automatic Reference Counting is not exception-safe by default in Objective-C. You should expect significant memory leaks from exceptions. In principle, ARC is exception-safe in Objective-C++, but @autoreleasepool blocks are still not released, which can lead to leaks on background threads. Making ARC exception-safe incurs performance penalties, which is one of many reasons to avoid significant use of Objective-C++. The Clang flag -fobjc-arc-exceptions controls whether ARC is exception-safe.

Catching and Reporting Crashes

iTunes Connect is supposed to provide crash reports, but it has a lot of limitations. Apple makes a single blanket request to the user for permission to upload crash reports. Many users decline. Reports are updated only once a day. iTunes Connect only supports applications deployed on the App Store, so you need a different system during development and internal betas. In short, if iTunes Connect works for you, great, but often it doesn’t.

The best replacement I’ve found is Quincy Kit (quincykit.net), which is integrated with HockeyApp (hockeyapp.net). It’s easy to integrate into an existing project, and it uploads reports to your own web server or the HockeyApp server after asking user permission. Currently, it doesn’t handle uploading logs to go along with the crash report.

Quincy Kit is built on top of PLCrashReporter from Plausible Labs. PLCrashReporter handles the complex problem of capturing crash information. Quincy Kit provides a friendly front end for uploading that information. If you need more flexibility, you might consider writing your own version of Quincy Kit. It’s handy and nice, but not all that complicated. You probably should not try to rewrite PLCrashReporter. While a program is in the middle of crashing, it can be in a bizarre and unknown state. Properly handling all of the subtle issues that go with that is not simple, and Landon Fuller has been working on PLCrashReporter for years. Even something as simple as allocating or freeing memory can deadlock the system and rapidly drain the battery. That’s why Quincy Kit uploads the crash files when the program restarts rather than during the crash. You should do as little work as possible during the crash event.

When you get your crash reports, depending on how your image was built, they may have symbols or they may not. Xcode generally does a good job of automatically symbolicating the reports (replacing addresses with method names) in Organizer as long as you keep the .dSYM file for every binary you ship. Xcode uses Spotlight to find these files, so make sure they’re available in a place that Spotlight can search. You can also upload your symbol files to HockeyApp if you’re using it for managing crash reports.

Errors and NSError

There’s a major difference between a user or environment error and a programming error. Programming errors need to be handled with exceptions in debug mode and with logging in release mode. If data corruption is possible, programming errors should also raise exceptions (crash) in release mode. Failure to allocate small amounts of memory needs to be treated as a programming error in iOS because it shouldn’t be possible and almost certainly indicates a programming error.

User errors or environment errors (network failures, disk full, and so on) should never raise exceptions. They should return errors, generally using an NSError object. NSFileManager is a good example of an object that uses NSError extensively.

- (BOOL)copyItemAtPath:(NSString *)srcPath

                toPath:(NSString *)dstPath

                 error:(NSError **)error

This method copies a file or directory from one location to another. Obviously that might fail for a variety of reasons. If it does, the method returns NO and updates an NSError object that the caller passes by reference (pointer to a pointer), as shown in this example.

  NSError *error;

  if (! [fileManager copyItemAtPath:srcPath

                             toPath:toPath

                             error:&error]) {

    [self handleError:error];

  }

This pattern is convenient because the return value is consistent with the success of the operation. If the method were, instead, to return an NSError, then nil would indicate success. This would be confusing and error prone.

Internally, the method might look something like this:

- (BOOL)copyItemAtPath:(NSString *)srcPath

                toPath:(NSString *)dstPath

                 error:(NSError **)error {

  

  BOOL success = ...;

  if (! success) {

    if (error != NULL) {

      *error = [NSError errorWithDomain:...];

    }

  }

  return success;

}

Note how this checks that error (a pointer to a pointer) is non-NULL before dereferencing it. This allows callers to pass NULL to indicate that they don’t care about the error details. They might still check the return value to determine the overall success or failure of the operation.

NSError encapsulates information about an error in a consistent package that is easy to pass around. Since it conforms to NSCoding, it’s also easy to write NSError objects to disk or over a network.

Errors are primarily defined by their domain and a code. The code is an integer, and the domain is a string that allows you to identify the meaning of that integer. For instance, in NSPOSIXErrorDomain, the error code 4 indicates that a system call was interrupted (EINTR), but in NSCocoaErrorDomain, the error code 4 indicates that a file was not found (NSFileNoSuchFileError). Without a domain, the caller would have to guess how to interpret the error code. You’re encouraged to create your own domains for your own errors. You should generally use a Uniform Type Indicator (UTI) for this, such as com.example.MyApp.ErrorDomain.

NSError includes a user info dictionary that can contain any information you like. There are several predefined keys for this dictionary, such as NSLocalizedDescriptionKey, NSUnderlyingErrorKey, and NSRecoveryAttempterErrorKey. You’re free to create new keys to provide domain-specific information. Several domains already do this, such as NSStringEncodingErrorKey for passing the relevant string encoding or NSURLErrorKey for passing an URL.

Error Localization

Where to localize errors is always a tricky subject. Low-level frameworks tend to present errors in very user-unfriendly ways. Errors like “Interrupted system call (4)” are generally not useful to the user. Translating such an error message into French and Spanish doesn’t help anything. It just wastes money and confuses users in more languages. Localizing these kinds of error messages makes things more difficult to debug because logs may be sent to you with errors in a language you can’t read.

This last point bears emphasizing. Never localize a string that you don’t intend to display to a user.

Because errors often need to be logged in the developer’s language, I recommend against using NSLocalizedDescriptionKey and its relatives in most cases for NSError. Instead, localize at the point of displaying the error. You can keep localized strings for various error codes using a localized string table with the same name as your error domain with .strings appended. For instance, for the error domain com.example.MyApp.ErrorDomain, you have a localized strings file named com.example.MyApp.ErrorDomain.strings. In that file, just map the error code to the localized value:

“1” = “File not found.”

Then, to read the file, just use NSBundle:

  NSString *key = [NSString stringWithFormat:@”%d”, [error code]];

  NSString *localizedMessage = [[NSBundle mainBundle]

                             localizedStringForKey:key

                                             value:nil

                                             table:[error domain]];

Error Handler Blocks

Blocks provide a very flexible way to handle errors. Passing error-handling blocks is particularly useful for asynchronous operations. They’re also useful as a more flexible version of error recovery attempters.

See the Error Handling Programming Guide’s (developer.apple.com/) section on “Error Responders and Error Recovery” for more information on error responders and error recovery attempters. Blocks are almost always a better error recovery solution on iOS because iOS offers no UI integration with error responders.

Often error handling can be easily combined with completion blocks. For example, the <NSFilePresenter> protocol includes this method:

- (void)savePresentedItemChangesWithCompletionHandler:

                 (void (^)(NSError *errorOrNil))completionHandler

This is an excellent example of using blocks to manage error handling. Because this method may perform time-intensive operations, it’s not possible to immediately inform the caller of success or failure. You might implement this method as follows:

- (void)savePresentedItemChangesWithCompletionHandler:

                 (void (^)(NSError *errorOrNil))completionHandler

{

  dispatch_queue_t queue = ...;

  // Dispatch to the background queue and immediately return

  dispatch_async(queue, ^{

    //

    // ... Perform some operations ...

    //

    if (completionHandler) {

      NSError *error = nil;

      if (anErrorOccurred) {

        error = [NSError errorWithDomain:...];

      }

      // Run the completion handler on the main thread

      dispatch_sync(dispatch_get_main_queue(), ^{

          completionHandler(error);

      });

    }

  });

}

This pattern is often more convenient for the caller than the typical error-handling delegate callbacks. Rather than defining a delegate method such as filePresenter:didFailWithError:, the caller can keep the error-handling code close to the calling code. The preceding method would be used like this:

[presenter savePresentedItemChangesWithCompletionHandler:^(NSError *e) {

  if (e) {

    ... respond to error ...

  }

  else {

    ... cleanup after success ...

  }];

Note that the previous code doesn’t require that completionHandler be stored as an ivar. This approach avoids the problem of retain loops. There may be a short-lived retain loop until the operation completes, but this is generally a good thing. As soon as the operation completes and the completion handler fires, the retain loop (if any) is automatically cleared. For more information on blocks and their patterns, see Chapter 23.

Logs

Logging is a critical part of debugging. It’s also very hard to get right. You want to log the right things, and you want to log in the right way. Let’s start with logging in the right way.

Foundation provides a single logging call: NSLog. The only advantage NSLog has is that it’s convenient. It’s inflexible and incredibly slow. Worst of all, it logs to the console, which is never appropriate in released code. NSLog should never appear in production code.

Some people deal with this issue simply:

#ifdef DEBUG

#define MYLog NSLog

#else

#define MYLog

#end

That’s fine for pulling out NSLog, but now you have no logs at all, which is not ideal. What you need is a logging engine that adapts to both development and release. Here are some of the things to consider in your logging engine:

It should log to console in debug mode and to a file in release mode. If you don’t log to console in debug mode, you won’t see logging output in Xcode. Ideally, it should be able to log to both at the same time.

It should include logging levels (error, warning, info, verbose).

It should make sure that logging to disabled logging levels is cheap.

It should not block the calling thread while it writes to a file or the console.

It must support log aging to avoid filling the disk.

It should be very easy to call, generally using a C syntax with varargs rather than an Objective-C syntax. The NSLog interface is very easy to use, and you want something that looks basically like that. You definitely don’t want simple logging statements to require multiple lines of code.

My current recommendation for iOS logging is Lumberjack from Robbie Hanson of Deusty Designs. See “Further Reading” at the end of this chapter for the link. In general, it requires only a few extra lines of code to configure, and a simple substitution of NSLog to DDLog.

This still leaves the question of what to log. If you log too little, you won’t have the information you need to debug issues. If you log too much, you’ll overwhelm even the best system, hurt performance, and age your logs so quickly that you probably still won’t have the information you need. Middle ground is very application-specific, but there are some general rules.

When adding a logging statement, ask yourself what you would ever do with it. Are you just relogging something that’s already covered by another log statement? This is particularly important if you’re logging data rather than just “I’m in this method now.”

Avoid calculating complex data if you might not log it. Consider the following code:

NSString *expensiveValue = [self expensiveCall];

DDLogVerbose(@”expensiveValue=%@”, expensiveValue);

If you never use expensiveValue in the upcoming code and verbose logging isn’t turned on, you’ve wasted time calculating it. Lumberjack is written in such a way that this kind of logging is efficient:

DDLogVerbose(@”expensiveValue=%@”, [self expensiveCall]);

This translates to

do {

  if(ddLogLevel && LOG_FLAG_VERBOSE)

    [DDLog log:...

           format:@”expensiveValue=%@”, [self expensiveCall]];

} while(0);

In this case, expensiveCall is not executed unless needed. The log level is checked twice (once in the macro and once in [DDLog log:...]), but this is a very fast operation compared to expensiveCall. If you build your own logging engine, this is a good technique to emulate.

A similar logging trick is to make sure you need to log before entering a loop. In Lumberjack, it’s done this way:

if (LOG_VERBOSE) {

  for (id object in list) {

    DDLogVerbose(@”object=%@”, object);

  }

}

The point is to avoid repeatedly calculating whether to log and to avoid calculating the log string. That’s even more important if complex work needs to be done to generate the log.

Most of the time, verbose logging is turned off, so even if DDLogVerbose checks the level again, the preceding code is cheaper in most cases. It also avoids creating a string for object. When verbose logging is turned on, the extra LOG_VERBOSE check is trivial compared to the rest of the loop.

Logging Sensitive Information

Logging opens up serious privacy concerns. Many applications process information that should never go into a log. Obviously, you should not log passwords or credit card numbers, but this is sometimes trickier than it sounds. What if sensitive information is sent over a network and you log the packets? You may need to filter your logs before writing them to avoid this situation.

Don’t ask your customers to “just trust you” with their sensitive information. Not only does that put the customer at risk, but the more of their information in your possession, the more legal issues you have to consider. Few things eat up profits as quickly as consulting lawyers.

Regularly audit your logs to make sure you’re not logging sensitive information. After running your program at the maximum logging level, search the logs for your password and any other sensitive information. If you have automated tests, this generally can be added fairly easily.

Encrypting your logs does nothing to help this situation. The problem is that the users send their logs to you, and you have the decryption key. If you feel you need to encrypt your logs, you’re probably logging something you shouldn’t be.

During development, it’s occasionally important to see the real data in the logs. I spent quite some time tracking down a bug where we were dropping the last character of the password. Had we logged the password, this bug would have been much easier to discover. If you need this kind of functionality, just make sure it doesn’t stay in place in production code.

Getting Your Logs

Logs aren’t very useful if you can’t get to them. Don’t forget to include some way to get the logs from the user. If you have a network protocol, you could upload them. Otherwise, you can use MFMailComposeViewController to send them as an attachment. Keep in mind the potential size of your logs. You often will want to compress them first. I’ve had good luck using Minizip for this (see “Further Reading”). There are some wrappers for Minizip such as Objective-Zip and ZipArchive, but I’m not particularly impressed with them.

TestFlight (testflightapp.com) supports uploading logs to their servers by simply using TFLog() instead of NSLog(). HockeyApp (hockeyapp.net) currently has no support for this.

Be sure to ask permission before sending logs. Not only are there privacy concerns, but sending logs can use a lot of bandwidth and battery. Generally, you only need to send logs in response to a problem report.

Summary

Error handling is one of the trickiest parts of any environment. It’s much easier to manage things when they go right than when they go wrong. In this chapter, you’ve seen how to best handle things when they go wrong. Nothing will make this an easy process, but you need to have the tools to make the process a manageable one.

Further Reading

Apple Documentation

The following documents are available in the iOS Developer Library at developer.apple.com or through the Xcode Documentation and API Reference.

Exception Programming Topics

Error Handling Programming Guide

TN2151: Understanding and Analyzing iPhone OS Application Crash Reports

Other Resources

Clang documentation, “Automatic Reference Counting.” This is the official documentation on how ARC and exceptions interact.clang.llvm.org/docs/AutomaticReferenceCounting.html#misc.exceptions

Lumberjack. Mac and iOS logger.github.com/robbiehanson/CocoaLumberjack

Olsson, Fredrik. “Exceptions and Errors on iOS,” Jayway Team Blog. A good discussion of programmer versus user errors and how to deal with exceptions versus other kinds of errors.www.blog.jayway.com/2010/10/13/exceptions-and-errors-on-ios

Quincy Kit. A nice crash-catcher for iOS. This is integrated into HockeyApp.quincykit.net

Volant, Gilles. zLib and Minizip. The standard for ZIP file handling. Don’t let the “win” and “Dll” fool you. This is highly portable.http://winimage.com/zLibDll/minizip.html

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.172.146