Learn Objective-C Lesson 12: Exception Handling

Because of Objective-C’s dynamic runtime there are cases where errors go unnoticed until runtime. These might include sending messages an object doesn’t respond to, or going out of bounds of an array. Sometimes you may not even catch these errors while you’re testing—for example, say you had an application that allowed users to access a value in an array by entering an index value. If the user entered ’20′ when there are only 5 values in the array, the index would be out of bounds; in your testing, you may never have thought to check such numbers.

Granted, the above example is rather contrived, but you should still try to anticipate such issues and do something about them, so your program doesn’t crash. Resolving these issues generally includes logging them and informing the user—an incorrect value, in the above example, or if it’s an error the user can’t fix (sending the wrong message), notify the user and then gracefully exit.

Runtime issues such as these are typically exceptions. If uncaught, your program will quit. Let’s look at an example.

  1. #import <Foundation/Foundation.h>
  2. int main(int argc, char *argv[]) {
  3.     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  4.     NSArray *array = [[NSArray alloc] initWithObject:@"A string"];
  5.     [array nonExistentMethod];
  6.     NSLog(@"Object at index 5 is %@", [array objectAtIndex:5]);
  7.     [array release];
  8.     NSLog(@"No issues!");
  9.     [pool drain];
  10.     return 0;
  11. }

Building the program nets you a warning that your array may not respond to nonExistentMethod (obviously). If we run it, we get a whole bunch of info:

  1. **2011-05-02 19:18:59.492 Exceptions[760:707] -[__NSArrayI nonExistentMethod]: unrecognized selector sent to instance 0x100113e90
  2. 2011-05-02 19:18:59.525 Exceptions[760:707] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI nonExistentMethod]: unrecognized selector sent to instance 0x100113e90'
  3. *** First throw call stack:
  4. (
  5.     0   CoreFoundation                      0x00007fff8f509406 __exceptionPreprocess + 198
  6.     1   libobjc.A.dylib                     0x00007fff941a09ea objc_exception_throw + 43
  7.     2   CoreFoundation                      0x00007fff8f584ade -[NSObject doesNotRecognizeSelector:] + 190
  8.     3   CoreFoundation                      0x00007fff8f4db8a3 ___forwarding___ + 371
  9.     4   CoreFoundation                      0x00007fff8f4d8178 _CF_forwarding_prep_0 + 232
  10.     5   Exceptions                          0x0000000100000e33 main + 195
  11.     6   Exceptions                          0x0000000100000d64 start + 52
  12.     7   ???                                 0x0000000000000003 0x0 + 3
  13. )
  14. terminate called throwing an exception**

Wading through that, we see some indications that array does indeed not respond to nonExistentMethod. For education’s sake, we’re going to keep that line, and instead use the @try…@catch() blocks. This is what our program now looks like:

  1. #import
  2.  
  3. int main(int argc, char *argv[]) {
  4.     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  5.     NSArray *array = [[NSArray alloc] initWithObject:@"A string"];
  6.     @try {
  7.         [array nonExistentMethod];
  8.     }
  9.     @catch (NSException *exception) {
  10.         NSLog(@"Caught exception %@", exception);
  11.     }
  12.     NSLog(@"Object at index 5 is %@", [array objectAtIndex:5]);
  13.     [array release];
  14.     NSLog(@"No issues!");
  15.     [pool drain];
  16.     return 0;
  17. }

The code in the @try block is your usual program code. The code in the @catch block is your error handling code—here, we just log the exception. In fact, the next NSLog will also cause an exception—an out of bounds exception. We can therefore move that line into the @try block.

  1. int main(int argc, char *argv[]) {
  2.     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  3.     NSArray *array = [[NSArray alloc] initWithObject:@"A string"];
  4.     @try {
  5.         [array nonExistentMethod];
  6.         NSLog(@"Object at index 5 is %@", [array objectAtIndex:5]);
  7.     }
  8.     @catch (NSException *exception) {
  9.         NSLog(@"Caught exception %@", exception);
  10.     }
  11.     [array release];
  12.     NSLog(@"No issues!");
  13.     [pool drain];
  14.     return 0;
  15. }

The results are now

  1. **2011-05-02 19:33:09.308 Exceptions[872:707] -[__NSArrayI nonExistentMethod]: unrecognized selector sent to instance 0x100113e90
  2. 2011-05-02 19:33:09.312 Exceptions[872:707] Caught exception -[__NSArrayI nonExistentMethod]: unrecognized selector sent to instance 0x100113e90
  3. 2011-05-02 19:33:09.313 Exceptions[872:707] No issues!**

Note now that our program no longer crashes, but instead catches the exceptions, and continues. Note that the @try…@catch block is exited after the first exception is caught, regardless of if there are any others.

You can also append a @finally { } block that executes regardless of whether there has been an exception or not.

Throwing Exceptions

Unlike in Java, Objective-C exceptions should not be thrown whenever there is an error—try to handle the error, or use something like NSError or the NSAssert() method.

As Apple states, *“You should reserve the use of exceptions for programming or unexpected runtime errors such as out-of-bounds collection access, attempts to mutate immutable objects, sending an invalid message, and losing the connection to the window server. You usually take care of these sorts of errors with exceptions when an application is being created rather than at runtime.

If you have an existing body of code (such as third-party library) that uses exceptions to handle error conditions, you may use the code as-is in your Cocoa application. But you should ensure that any expected runtime exceptions do not escape from these subsystems and end up in the caller’s code. For example, a parsing library might use exceptions internally to indicate problems and enable a quick exit from a parsing state that could be deeply recursive; however, you should take care to catch such exceptions at the top level of the library and translate them into an appropriate return code or state.”*

Throwing exceptions is a rather “expensive” procedure, and of course uncaught exceptions will cause your program to crash. That being said, throwing an exception is straightforward:

  1. NSException* myException = [NSException
  2.         exceptionWithName:@"IndexOutOfBoundsException"
  3.         reason:@"Attempted to access an array index that is out of bounds"
  4.         userInfo:nil];
  5. @throw myException;
  6. // [myException raise]; /* equivalent to throwing the exception, above */

A more popular usage is in the case where you can’t recover from the issue (invalid data, for example, or where you tried to access an index that is beyond the size of an array)—you could catch the exception to perform some cleanup, then throw it back to the system for something else to catch, or to quit at that point.

Exceptions provide a powerful and flexible way to handle issues in your code. Use them wisely—don’t make the runtime system a massive juggling round.

This post is part of the Learn Objective-C in 24 Days course.

Author: Feifan Zhou