MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Understanding Exceptions and Handlers in Cocoa

by Jose Cruz
07/31/2007

Introduction

Most, if not all, software products will have errors. These errors may come from design flaws in the product. They may be due to problems between the product and the underlying system. They may be caused by unexpected user actions. Whatever the origin, errors do not give a good user experience.

Users, however, do not only judge a software product by the errors it generates. They also judge the product on how it handles those errors. For instance, they see a product that crashes or freezes often during normal usage as poorly made. They may also have the same view of a product that displays frequent error messages. And they will dislike a product that loses some, if not all, of their data after an error.

On the other hand, these users may find a software product that still works even after an error as desirable. They may also prefer a product that exits gracefully after a fatal error. In order for software products to have such features, they will have to use an exception handling system.

This article focuses on how to use Cocoa to build an exception handling system. First, it shows the classes and keywords that make up that system. Next, it shows how to prepare and raise a Cocoa exception and then how to intercept and process it. Finally, it talks about the issue of uncaught exceptions and the concept of a default exception handler.

Exceptions in Cocoa

An exception handling system is a set of signals and code that handles an unexpected error. First, the affected code raises a signal known as an exception. The signal moves outwards until it reaches a code segment known as a handler. The handler tries to process the error or, failing that, sends it to the next available handler. But if the signal reaches the topmost handler, it becomes an uncaught exception. The topmost or default handler saves any user data and does any clean-up chores before it shuts down the product.

The Cocoa framework provides the means to build a robust exception system. Not only can that system detect and process errors, it can also provide information about those errors. The class that makes these possible is the NSException class.

The NSException Class

The NSException class is the main component of the Cocoa exception system. It directly derives from the NSObject class (Figure 1). It has three private properties: name and reason, which are both NSStrings; and userInfo, which is an NSDictionary.

The NSException class
Figure 1. The NSException class.

The name property is the unique ID of the exception. You can use a predefined name from the framework, or you can provide your own (See the Predefined Exceptions section later in this article). The reason property describes the cause of the exception. Finally, the userInfo property contains more information about the exception.

Preparing an NSException

To create an instance of NSException, send an exceptionWithName:reason:userInfo: message to the class. The following example creates an NSException named DemoException. It also sets the reason property to the NSString "A test exception". It then stores the instance into the variable anException. Notice that the userInfo property is set to a nil value.

NSException    *anException;
//..
anException = [NSException exceptionWithName:@"DemoException" 
        reason:@"A test exception"
        userInfo:nil];

If you want NSException to have more data about the error, use the userInfo property to contain the data. Listing 1 shows one example of how this is done. First, the dictionaryWithObjectsAndKeys: message creates the NSDictionary instance aUserInfo. It initializes the instance with data from aLabel and anOutput. Next, the exceptionWithName:reason:userInfo: message creates an instance of NSException. It then sets the userInfo property to aUserInfo.

Listing 1. Setting the userInfo property
NSDictionary    *aUserInfo;
NSString    *aLabel;
NSNumber     *anOutput;
//..
aLabel = [NSString stringWithString:@"The last output value:"];
anOutput = [NSNumber numberWithLong:987654321];
//..
aUserInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        aLabel, @"state",
        anOutput, @"output", nil];
anException = [NSException exceptionWithName:@"anException" 
            reason:@"A test exception with more data"
            userInfo:aUserInfo];

Suppose you already have an instance of NSException. To update the instance with new error data, use the initWithName:reason:userInfo: message. The following example shows the message changing the name and reason properties of the NSException instance, anException.

[anException initWithName:@"newError" 
            reason:@"This is a new error" 
            userInfo:nil];

The following are a few things for you to keep in mind when creating an instance of the NSException class.

Raising an NSException

To send an NSException on its way, pass a raise message to the instance (see below). The instance first locates the nearest exception handler. Next, it starts a longjump operation, which saves the current code state, and transfers control to the handler. The handler then tries to process the exception.

[anException raise];

If the instance fails to locate a handler, it goes straight to the default exception handler. The section When Handlers Fail covers the default handler in more detail.

You can also pass data to the NSException when you send it a raise message. The data is then stored into the reason property as a formatted NSString. There are two ways you can pass data when raising an NSException. One way is to use the raise:format:, message. The first argument of the message is the unique ID. The second argument is the C-style format string as an NSString. The third is a fixed list of data values. In short, this message works similarly to the printf() C-function.

The following example raises an NSException named "Invalid value". It sets the reason property of the raised instance to the phrase "The value is 3054". It also sets the userInfo property of that instance to nil.

NSString    *aFormat;
long        aValue;
//...
aFormat = [NSString stringWithString:@"The value is %d"];
aValue = 3054;
//...
[NSException raise:@"Invalid value" format:aFormat, aValue];

Another way is to use a raise:format:arguments: message. Again, the first argument of the message is the unique ID. The second argument is the C-style format string. The third is a va_list variable that refers to a variable list of arguments. This message works similarly to the vprintf() C-function.

For example, assume that the Cocoa class aClass has the method demoMethod, as shown in the following code. The input arguments of demoMethod start with the integer theCount and end with an ellipsis. This means the method has a variable list of input arguments. Since Objective-C reads the list from right to left, theCount is the last argument of that list.

- (void)demoMethod:(int)theCount,...
{
    // ...
    // your code goes here...
}

To access the arguments list, first declare a local variable of type va_list.

va_list        a_list;

Next, call the va_start() function, passing the va_list local and the last input argument. This sets the va_list local to the contents of the variable list.

va_start(a_list, theCount);

Send the raise:format:arguments: message to the NSException class. Make sure to supply a format string that sees some, if not all, of the input arguments.

Listing 2 shows how demoMethod deals with variable input arguments. If you send the message [aClass demoMethod:23, 64, 256], the class raises an NSException with the name "Invalid arguments". The reason property of the instance is set to the string "Arguments List = 0x40, 0d256". Its userInfo property is still set to a nil.

Listing 2. Handling variable arguments
- (void)demoMethod:(int)theCount,...
{
    NSException    *anException;
    NSString    *aFormat;
    va_list        aList;
    
    // prepare to access the variable input arguments
    va_start(aList, theCount);
    //...
    // something happened to cause an exception
    //...
    aFormat = [NSString stringWithString:@"Argument List = 0x%x, 0d%u"];
            [NSException raise:@"Invalid arguments" 
            format:aFormat
            arguments:aList];
    //...
}

Predefined Exceptions

The Cocoa framework comes with a large set of predefined exception names. These unique NSString constants describe specific error states. Most, if not all, Cocoa classes use these predefined names when they send their exception signals.

The framework that shipped with Xcode 2.4 has at least 60 predefined exception names. Most of the names are listed in two header files: NSException.h and NSErrors.h. Describing every predefined name is beyond the scope of this article, but you can find a list of these names in the Foundation Constants document, which is part of the ADC Reference library.

Check the list for a predefined name that describes your code's error state. Once you find the right name, use it to raise an exception for that state. For instance, to report an out-of-state error, use an NSRangeException. The following sample code first creates an instance of NSException using the NSRangeException name. It then passes a raise message to the instance to send it on the way.

anException = [NSException exceptionWithName:NSRangeException 
        reason:@"Attempted to read beyond the string."
        userInfo:nil];
[anException raise];

To report a write-to-text error, use an NSTextWriteException. The sample code below uses a raise:format, message to create an instance of NSException. It sets the name property of the instance to NSTextWriteException, and the reason property to the string "Tried to write to the file..."

[NSException raise:NSTextWriteException 
    format:@"Tried to write to the file %@"
    , aFileName];

Using a predefined name keeps the code consistent and predictable. It allows other Cocoa classes to correctly identify and process the raised exception. Make sure to use the reason and userInfo properties to differentiate your code from those raised by the framework.

Handling Exceptions in Cocoa

The aim of an exception handler is to quietly trap and process an incoming exception. At best, users should be unaware that an error occurred, unless they prefer to be. At worst, users get a modal dialog that warns them of the fatal error (Figure 2).

A typical fatal error dialog
Figure 2. A typical fatal error dialog

There are two ways to write exception handlers in Cocoa. One way is to use NS_ macros, the other is to use the new Objective-C keywords: @try, @catch, and @finally. Handlers written with these keywords are supported only on version 10.3 and newer of Mac OS X. Handlers written with NS_ macros, however, are supported by all versions of Mac OS X.

Apple recommends using the Objective-C keywords to write exception handlers. For that reason, this article will focus only on that approach. But if you want to learn about NS_ macros, read its entry in the ADC document on exception programming.

The Basic Exception Handler

Listing 3 shows the basic structure of an exception handler. The @try keyword sets the start of the exception-handling block. This block has code that may generate an exception. The @catch keyword sets the start of the local handler block. This block has code that will process the trapped exception. It also has code that can resend the exception when required. The @finally keyword sets the start of the housekeeping block. This block has code that performs any clean-up tasks needed. Unlike the first two blocks, this block is optional.

Listing 3. A basic exception handler
@try
{
    //...
    // the exception handling domain
    //...
}
@catch (NSException *theErr)
{
    //...
    // the local exception handler
    //...
}
@finally
{
    //...
    // the housekeeping domain
    //...
}

Figure 3 shows the flow of control within an exception handler. If an exception occurs, the @try block transfers control to the @catch block. Once the @catch block finishes, it transfers control to the @finally block. But if the @try block finishes without any exceptions, it also transfers control to the @finally block. In short, the @finally block is always executed whether or not an exception has occurred.

The flow of control within the handler
Figure 3. The flow of control within the handler

To use the new keywords in your Xcode project, first choose Edit Project Settings from the Project menu. From the Project Info dialog, click on the Build tab to display a list of build settings. Then choose Language from the drop-down list Collection. Scroll down the list until you see an entry for Enable Objective-C Exceptions (Figure 4). Click on the check box to select this option.

Enabling the compiler keywords
Figure 4. Enabling the compiler keywords

If you are using an older version of Xcode, scroll down the list until you see an entry for Other C Flag (Figure 5). Click on the cell next to that entry and type -fobjc-exceptions. Xcode will send this setting to GCC each time it builds the Cocoa project.

Setting the option flags
Figure 5. Setting the option flags

Handling an NSException

To see how an exception handler process an NSException, first we need a source of exceptions. Listing 4 shows a method named divideLong:by:. This method takes two NSNumbers as its input arguments. It then checks aDivisor to see if it has a zero value. If true, the method raises an instance of NSInvalidArgumentException. Otherwise, it divides the two numbers and returns the result as an NSNumber.

Listing 4. Dividing two NSNumbers
- (NSNumber *)divideLong:(NSNumber *)aDividend by:(NSNumber *)aDivisor
{
    NSException *loc_err;
    long     loc_long;
    
    // validity check
    loc_long = [aDivisor longValue];
    if (loc_long == 0)
    {
        // create and send an exception signal
        loc_err = [NSException 
            exceptionWithName:NSInvalidArgumentException
            reason:@"Division by zero attempted" 
            userInfo:nil];
        [loc_err raise];
    }
    else
        // perform the division
        loc_long = [aDividend longValue] / loc_long;
    
    // return the results
    return ([NSNumber numberWithLong:loc_long]);
}

To trap the NSException, write the exception handler as shown in Listing 5. The @try block first assigns integer values to two NSNumber locals. Next, it passes those same locals to the divideLong:by: method. It then stores the result of the method into another NSNumber local, and displays it using the NSLog() function. But if an exception occurs, the @try block transfers control to the @catch block, which then displays the exception using its NSLog() function.

Notice that the @finally block is included as a placeholder. Even though it is optional, it is good practice to have it around should you decide to add any clean-up code. Feel free to exclude the block if it makes your code more readable.

Listing 5. Trapping a single NSException
NSNumber *tst_dividend, *tst_divisor, *tst_quotient;
// prepare the trap
@try
{
    // initialize the following locals
    tst_dividend = [NSNumber numberWithLong:8];
    tst_divisor = [NSNumber numberWithLong:2];
    
    // attempt a division operation
    tst_quotient = [self divideLong:tst_dividend by:tst_divisor];
    
    // display the results
    NSLog (@"The answer is: %@", tst_quotient);
}
@catch (NSException *theErr)
{
    // an exception has occured
    // display the results
    NSLog (@"The exception is:\n name: %@\nreason: %@"
        , [theErr name], [theErr reason]);
}
@finally
{
    //...
    // the housekeeping domain
    //...
}

Run the code in Listing 5 as is. It will display the following message at the Xcode console window.

2006-11-26 03:52:30.004 DemoApp[506] The answer is: 4

Next, change the code so that tst_divisor gets a zero value.

tst_divisor = [NSNumber numberWithLong:0];

Run the example code again. This time, the local object tst_quotient remains set to a nil. Also, the NSLog() function in the @try block is skipped over. Instead, the NSLog() function in the @catch block will display the following console message.

2006-11-26 04:06:03.654 DemoApp[569] The exception is:
    name: NSInvalidArgumentException
    reason: Division by zero attempted

Handling Multiple NSExceptions

The basic handler can also trap multiple NSExceptions. To see how this is done, first change the divideLong:by: method as shown in Listing 6. This time, the method checks if one of its input arguments is a nil. If this is true, the method raises an instance of NSInternalInconsistencyException.

Listing 6. Generating multiple NSExceptions
- (NSNumber *)divideLong:(NSNumber *)aDividend 
        by:(NSNumber *)aDivisor
{
    NSException *loc_err;
    long loc_long;
    
    // validity check
    if ((aDividend == nil) || (aDivisor == nil))
    {
        // create and send an exception signal
        loc_err = [NSException 
            exceptionWithName:NSInternalInconsistencyException
            reason:@"Nil input arguments are sent" 
            userInfo:nil];
        [loc_err raise];
    }
    else
    {
        loc_long = [aDivisor longValue];
        if (loc_long == 0)
        {
            // create and send an exception signal
            loc_err = [NSException 
                exceptionWithName:NSInvalidArgumentException
                reason:@"Division by zero attempted" 
                userInfo:nil];
            [loc_err raise];
        }
        else
            // perform the division
            loc_long = [aDividend longValue] / loc_long;
    }
    // return the results
    return ([NSNumber numberWithLong:loc_long]);
}

Then change the @catch block as shown in Listing 7. The block now checks the name property of the NSException, theErr. It then displays the right log message for that name. If the block failed to process the NSException, it sends a raise message to send the NSException to the next handler.

Listing 7. Trapping Multiple NSExceptions
NSNumber *tst_dividend, *tst_divisor, *tst_quotient;
NSString *tst_name;

// prepare the trap
@try
{
    // initialize the following locals
    tst_dividend = [NSNumber numberWithLong:8];
    tst_divisor = [NSNumber numberWithLong:2];
    
    // attempt a division operation
    tst_quotient = [self divideLong:tst_dividend 
                by:tst_divisor];
    
    // display the results
    NSLog (@"The answer is: %@", tst_quotient);
}
@catch (NSException *theErr)
{
    tst_name = [theErr name];
    if ([tst_name  isEqualToString:NSInvalidArgumentException])
        NSLog (@"The answer is: INFINITY");
    else 
    {
        if ([tst_name isEqualToString:NSInternalInconsistencyException])
            NSLog (@"The answer is: UNDEFINED");
        else
            [theErr raise];
    }
}
@finally
{
    //...
    // the housekeeping domain
    //...
}

Run the example code as is. It will still display the following message on the console window.

2006-12-06 03:52:30.004 DemoApp[506] The answer is: 4

Next, set the variable tst_divisor to zero. The code then displays a different message to the window.

2006-12-06 03:55:30.014 DemoApp[506] The answer is: INFINITY

Finally, comment out the following two lines of code. This sets the variables tst_dividend and tst_divisor to nil.

// comment these lines out
// tst_dividend = [NSNumber numberWithLong:8];
// tst_divisor = [NSNumber numberWithLong:2];,

Now, run the modified code. It displays another message to the console window.

2006-12-06 04:10:10.040 DemoApp[506] The answer is: UNDEFINED

Handling Other Cocoa Objects

Finally, the basic handler can trap other Cocoa objects besides an NSException. Simply use the @throw keyword to send the Cocoa object to the handler. Then use the @catch keyword to trap that object.

To demonstrate, modify the divideLong:by: method as shown in Listing 8. The method stores the division results into the NSNumber local, loc_quot. Then it uses the @throw keyword to send the result to the handler. Notice that the return type for the method is changed to a void. Compare this method to those shown in Listing 4 and Listing 6.

Listing 8. Using the @throw directive
- (void)divideLong:(NSNumber *)aDividend 
             by:(NSNumber *)aDivisor
{
    NSException     *loc_err;
    NSNumber        *loc_quot;
    long         loc_long;
    
    // validity check
    if ((aDividend == nil) || (aDivisor == nil))
    {
        // create and send an exception signal
        loc_err = [NSException exceptionWithName:NSInternalInconsistencyException
                reason:@"Nil input arguments are sent" 
                userInfo:nil];
        [loc_err raise];
    }
    else
    {
        loc_long = [aDivisor longValue];
        if (loc_long == 0)
        {
            // create and send an exception signal
            loc_err = [NSException exceptionWithName:NSInvalidArgumentException
                    reason:@"Division by zero attempted" 
                    userInfo:nil];
            [loc_err raise];
        }
        else
        {
            // perform the division
            loc_long = [aDividend longValue] / loc_long;
            
            // return the results using @throw
            loc_quot = [NSNumber numberWithLong:loc_long];
            @throw loc_quot;
        }
    }
}

Now, modify the exception handler as shown in Listing 9. The second @catch block has an NSNumber as its argument. It retrieves the value of the argument and displays it using the NSLog() function. Also, if you run the modified code, you will see the same set of messages in the Xcode console window.

Listing 9. Trapping a thrown Cocoa object
NSNumber *tst_dividend, *tst_divisor, *tst_quotient;
NSString *tst_name;

// prepare the trap
@try
{
    // initialize the following locals
    tst_dividend = [NSNumber numberWithLong:8];
    tst_divisor = [NSNumber numberWithLong:2];
    
    // attempt a division operation
    tst_quotient = [self divideLong:tst_dividend 
                    by:tst_divisor];
    
    // display the results
    NSLog (@"The answer is: %@", tst_quotient);
}
@catch (NSException *theErr)
{
    tst_name = [theErr name];
    if ([tst_name isEqualToString:NSInvalidArgumentException])
        NSLog (@"The answer is: INFINITY");
    else 
    {
        if ([tst_name isEqualToString:NSInternalInconsistencyException])
            NSLog (@"The answer is: UNDEFINED");
        else
            [theErr raise];
    }
}
@catch (NSNumber *theNum)
{
    // display the number results
    NSLog (@"The answer is: %@", theNum);
}
@finally
{
    //...
    // the housekeeping domain
    //...
}

Using the @throw keyword to return results has its own drawbacks.

When Handlers Are Nested

Sometimes, an exception handler is placed within another. This placement, called nesting, can happen either by consequence or by design. Learning how control flows inside nested handlers can help in debugging a faulty handler or in tracing an error signal.

Types of Nesting Structures

Exception handlers can be nested in three ways.

Listing 10 is an example of the first nesting structure. Here, demoMethod has two exception handlers. The outer @try block of the first handler contains the @try…@catch…@finally block of the second handler. This type of nesting often happens by design.

Notice that the two @catch blocks use different names for their NSException variable. This allows the handlers know which NSException to trap at that time. In the example shown, the inner handler traps only NSExceptions with the ID Inner Error, while the outer handler traps those with the ID Outer Error.

Listing 10. Handlers nested in the same method
- (void)demoMethod
{
    // ...
    // code before the handler
    // ...
    // start of the outer handler
    @try
    {
        // ...
        // code that could generate an outer exception
        // ...
        
        // start of the inner handler
        @try
        {
            // ...
            // code that could generate an inner exception
            // ...
        }
        @catch (NSException *theErr2)
        {
            // identify the exception
            if ([[theErr2 name] isEqualToString:@"Inner Error"])
                // ...
                // code to handle the inner exception
                // ...
            else
                [theErr2 raise];
        }
        @finally
        {
            // ...
            // finish the inner handler
            // ...
        }
    }
    @catch (NSException * theErr1)
    {
        // identify the exception
        if ([[theErr1 name] isEqualToString:@"Outer Error"])
            // ...
            // code to handle the outer exception
            // ...
            else
                [theErr1 raise];
    }
    @finally
    {
        // ...
        // finish the outer handler
        // ...
    }
}

Listing 11 is an example of the second nesting structure. The two methods, demoA and demoB, have their own exception handlers. Also, the @try block in the method demoA calls the method demoB.

This type of nesting is usually a result of subclassing. A subclass may have a method in its @implementation block calling a method from the parent class. If the parent method has an exception handler, that handler is not nested when the method call is outside the subclass's @try…@catch. But if the method call is within the latter's @try…@catch block, then the parent's handler is nested in the subclass's handler.

Listing 11. Handlers nested in different methods
- (void) demoA
{
    // ...
    // code before the handler
    // ...
    // start of the outer handler
    @try
    {
        // ...
        // code that could generate an outer exception
        // ...
        [self demoB];
        // ...
    }
    @catch (NSException * theErr1)
    {
        // identify the exception
        if ([[theErr1 name] isEqualToString:@"Outer Error"])
            // ...
            // code to handle the outer exception
            // ...
            else
                [theErr1 raise];
    }
    @finally
    {
        // ...
        // finish the outer handler
        // ...
    }
    // ...
}

- (void)demoB
{
    // ...
    // code before the handler
    // ...
    // start of the inner handler
    @try
    {
        // ...
        // code that could generate an inner exception
        // ...
    }
    @catch (NSException * theErr2)
    {
        // identify the exception
        if ([[theErr2 name] isEqualToString:@"Inner Error"])
            // ...
            // code to handle an inner exception
            // ...
            else
                [theErr2 raise];
    }
    @finally
    {
        // ...
        // finish the inner handler
        // ...
    }
    // ...
}

Listing 12 is an example of the third nesting structure. This example shows two classes, DemoOne and DemoTwo. The instance method, demoMethod, in each class has it own @try…@catch…@finally block. Also, the @try block in DemoOne's demoMethod calls the demoMethod in DemoTwo.

This type of nesting is often the result of composition. A Cocoa class that uses other Cocoa classes in its methods may find its handlers nested around handlers from other classes.

Listing 12. Handlers nested in different classes
@implementation DemoOne
- (void)demoMethod
{
    DemoTwo *theClass;
    // ...
    // code before the handler
    // ...
    // start of the level 1 handler
    @try
    {
        // ...
        // code that could generate a level 1 exception
        // ...
        [theClass demoMethod];
    }
    @catch (NSException *theErr)
    {
        // identify the exception
        if ([[theErr name] isEqualToString:@"Outer Error"])
            // ...
            // code to handle a level 1 exception
            // ...
            else
                [theErr raise];
    }
    @finally
    {
        // ...
        // finish the level 1 handler
        // ...
    }
}
@end

@implementation DemoTwo
- (void)demoMethod
{
    // ...
    // code before the handler
    // ...
    // start of the level 2 handler
    @try
    {
        // ...
        // code that could generate a level 2 exception
        // ...
    }
    @catch (NSException *theErr)
    {
        // identify the exception
        if ([[theErr name] isEqualToString:@"Inner Error"])
            // ...
            // code to handle a level 2 exception
            // ...
            else
                [theErr raise];
    }
    @finally
    {
        // ...
        // finish the level 2 handler
        // ...
    }
}
@end

The Flow of Control

Figure 6 shows the basic flow of control for two nested handlers. This flow remains the same regardless of the type of nesting structure. It also scales well for structures with more than two nested handlers.

Control flow within two nested traps.
Figure 6. Control flow within two nested traps

In the example above, the @try block in the outer handler transfers control to the inner handler (green). Then the @try block in the inner handler raises an exception. This exception is first trapped by the @catch block in the same handler (red). After that block handles the exception, it transfers control to the @finally block of the inner handler. Then the inner handler transfers control to the @finally block of the outer handler.

If the inner handler fails to handle the exception, it sends the latter to the outer handler (grey). The @catch block of the outer handler traps the exception and tries to handle it. When it does, it transfers control to the @finally block (blue). Otherwise, it sends the exception to the next possible handler (grey). But the @catch block still transfers control to the @finally block before sending the uncaught exception on its way.

Now when the @try block in the outer handler raises an exception, the flow of control (blue) is the same as that of a single handler. Since the exception occurs before or after the call to the inner handler, control is never transferred to the latter.

When Handlers Fail

If an exception handler failed to process an NSException, it should send the exception to the next available handler. But if all handlers fail, the exception becomes an uncaught exception. A special function called the default exception handler gets to trap these exceptions.

The Default Exception Handler

The default exception handler serves as a catchall for all uncaught exceptions. It is located at the end of the exception handling chain. Its aim is to gracefully stop the application in response to the exception.

Most Cocoa software is built around the NSApplication class, which installs its own default exception handler at launch time. When the default handler gets an uncaught exception, it does the following tasks in order.

You can control how the default exception handler behaves by changing one of two global masks. To access these masks, you need to add the NSExceptionHandler framework to your project. First, click on the Frameworks group on the Groups & Files pane of your project window. Choose Add to Project from the Projects menu, and use the dialog to navigate to the /System/Library/Frameworks directory. Select the bundle ExceptionHandling.framework, and click on the OK button to add it to the project. Finally, add the following statement to the source file that will access the masks.

#import <ExceptionHandling/NSExceptionHandler.h>

The Exception Handling Mask

The Exception Handling Mask controls how the default handler reacts to top-level and low-level exceptions. It also selects the error signals that the handler should trap. In fact, the default handler can trap two other signals: system-level exceptions and runtime errors. These signals are defined as follows.

Figure 7 shows the bit structure of the mask. Though the mask is 19 bits long, only the lower 9 bits are available for use. The rest are reserved by Apple for future settings.

Bit structure of the exception handling mask.
Figure 7. Bit structure of the exception handling mask

Bits zero to five are the handler bits. They tell the default handler how to react to the each error signal. Three of the bits tell the handler to log the signal. The other three tell the handler to process the signal.

Bits six to nine are the debugging bits. They tell the default handler how react to top-level or low-level exceptions. Again, two of the bits tell the handler to log the exceptions. The other two tell it to process the exceptions.

Assume you want the default handler to log system-level exceptions. You also want it to process runtime errors. Since you use your own handler to trap uncaught exceptions, you want the default handler not to deal with those exceptions.

To change the mask, first send a defaultExceptionHandler message to the NSExceptionHandler class. The class then returns an instance of the default handler. In the example below, the instance is stored in the local variable aHandler.

NSExceptionHandler     *aHandler;
//...
aHandler = [NSExceptionHandler defaultExceptionHandler];

Next, send an exceptionHandlingMask message to the instance. The instance returns the current mask settings as an unsigned integer.

unsigned int *theMask;
//...
theMask = [aHandler exceptionHandlingMask];

Now, check Figure 7 for the desired bit settings. Then change the bit mask as follows:

theMask = NSLogUncaughtSystemExceptionMask | NSHandleUncaughtRuntimeErrorMask;

When done, send a setExceptionHandlingMask: message to the NSExceptionHandler instance. Pass the changed mask as an unsigned integer.

[aHandler setExceptionHandlingMask:theMask];

If you want the default handler to log any top-level exceptions as well, change the bit mask as follows:

theMask = NSLogUncaughtSystemExceptionMask | NSHandleUncaughtRuntimeErrorMask
        | NSLogTopLevelExceptionMask;

Use this new mask to update the default handler as directed earlier.

The Exception Hanging Mask

The Exception Hanging Mask controls how the default handler uses an external debugger. It is 10 bits long, and its bit structure is shown in Figure 8. Only the lower five bits of the mask can be used. The rest are reserved by Apple for future settings. Also, the lower three bits select the error signal. The upper three bits select the exception level.

Bit structure of the exception hanging mask
Figure 8. Bit structure of the exception hanging mask

Assume you want system-level exceptions and top-level exceptions sent to the debugger. First, create an instance of NSExceptionHandler. Then send an exceptionHangingMask message to the instance. The instance returns the current mask settings as an unsigned integer.

unsigned int *theMask;
//...
theMask = [aHandler exceptionHangingMask];

Check Figure 8 for the correct bit settings. Set the bit mask as shown.

theMask = NSHangOnUncaughtSystemExceptionMask | NSHangOnTopLevelExceptionMask;

After making your changes, send a setExceptionHangingMask: message to the instance. Pass the new bit mask as an unsigned integer.

[aHandler setExceptionHangingMask:theMask];

Installing a Default Exception Handler

There are cases where you are unable to use the default handler from NSApplication. One such case is where your application project does not use NSApplication. Another is where your project is a plug-in or class framework. For these cases, Cocoa gives you the means to install your own custom default handler.

In order for your custom handler to work, make sure it conforms to the function prototype defined in the header file, NSException.h. The following shows how that handler should appear. Notice the handler uses a void for a return value. Also, notice that it takes a single NSException as its input argument.

void your_custom_handler(NSException *anException);
{
    //...
    // your handler code goes here
    //...
}

Cocoa provides two global functions to install and retrieve your custom handler. Both functions use function pointers to refer to your handler. Assume you have a custom default handler named aDefaultHandler. To install the handler, use the function NSSetUncaughtExceptionHandler(). Pass a pointer to the handler as the input argument.

NSSetUncaughtExceptionHandler(&aDefaultHandler);

To retrieve the handler, use the function NSGetUncaughtExceptionHandler(). The function returns a pointer to the handler. De-reference the pointer, and pass the NSException object as its input argument.

NSException     *theError;
unsigned int     thePointer;
//...
thePointer = NSGetUncaughtExceptionHandler();
(*thePointer)(theError);

You can also invoke your custom handler directly from the function call itself.

(*NSGetUncaughtExceptionHandler())(theError);

Always install your custom handler at the very start of the software. There are two places where you can do this. One is in the init() method of your main controller, as the following shows.

- (id)init
{
    if (self = [super init])
    {
    //...
    // your init code goes here
    //...
    NSSetUncaughtExceptionHandler(&aDefaultHandler);
    }
    return (self);
}

Another is in the main() function call itself, as shown in the following code example. Make sure to install your custom handler after you initialized the main autorelease pool. Also, make sure to import the NSException.h file into the source file that has the main controller class or the main() function.

int main (int argc, const char * argv[]) 
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    NSSetUncaughtExceptionHandler(&aDefaultHandler);
    //...
    // the rest of your code goes here
    //...
    [pool release];
    return 0;
}

Replacing the Default Exception Handler

You can also replace the default handler from NSApplication with your own handler. Be aware that this is a tricky process and may not work in some cases. It may also cause the Cocoa application to behave unexpectedly. Make sure to test your changes against every major version of Mac OS X.

To replace the default handler, first set the delegate controller for NSApplication. You can do this in two ways. One way is to use Interface Builder as follows:

  1. Double-click on the MainMenu.nib from your Xcode project. This action opens the nib bundle in Interface Builder.
  2. From the Instances panel, control-drag a line from the File Owner icon to the icon of your delegate controller. This displays the Inspector window for File Owner, which is an instance of NSApplication.
  3. Select the entry named delegate from the Outlets panel of that window. Click on the Connect button to complete the assignment.

Figure 9 shows an example of how a delegate is set using Interface Builder. In this example, the DemoTraps instance is the chosen delegate controller for File Owner.

Attaching a delegate controller
Figure 9. Attaching a delegate controller

Another way is to use the global object NSApp. This object is a shared instance of NSApplication. To set its delegate controller, send a setDelegate: message to NSApp. Pass a reference to the controller class as the input argument. For example, to set the DemoTraps class as the delegate, send the message as the following shows:

[NSApp setDelegate:DemoTraps];

You can also have the delegate controller itself send the setDelegate: message. The best place to do this is in the awakeFromNib method of that controller. Make sure to pass self as the input argument, as shown.

- (void)awakeFromNib
{
    //...
    [NSApp setDelegate:self];
    //...
}

Use the chosen delegate controller to install your custom default exception handler. First, add the method applicationDidFinishedLaunching: to the @implementation block of the controller. Then call the global function NSSetUncaughtExceptionHandler() from that method. Pass the pointer to your default handler as the input argument. Listing 13 shows how the custom handler aDefaultHandler is installed using these steps.

Listing 13. Installing the custom default exception handler
void aDefaultHandler(NSException *anException);
{
    //...
    // your handler code goes here
    //...
}

@implementation DemoTraps
    - (void)applicationDidFinishLaunching:(NSNotification *)aSignal
    {
    // install the custom default handler
    NSSetUncaughtExceptionHandler(&aDefaultHandler);
    }
@end

To force NSApplication to use your custom handler, open the header file of your delegate controller. Add the following @interface block to that file. This block declares the category method reportException: and attaches it to NSApplication. The category method overrides the one provided by NSApplication.

@interface NSApplication ( DemoApp ) 
    - (void)reportException:(NSException *)anException;
@end

Now add the following @implementation block to the source file of your delegate controller. This block defines the reportException: method. The method gets the uncaught exception from NSApplication. Then it retrieves the custom default handler and passes the exception to that handler for processing.

@implementation NSApplication ( DemoApp )
- (void)reportException:(NSException *)anException
{
    (*NSGetUncaughtExceptionHandler())(anException);
}
@end

Concluding Remarks

The Cocoa framework gives you the means to add an exception handling system in your software project. You can use the NSException class to raise an exception signal and carry information about the exception. You can also use the new Objective-C keywords of @try, @catch, and @finally to build an exception handler to trap NSExceptions and other Cocoa objects as well.

The framework also provides a default exception handler through its NSApplication class. You can change the behavior of this handler from two global flags or replace it with your own custom handler. And, if you are not using NSApplication, you can install your own custom handler by using two Cocoa global functions.

Exception handling helps improve the user experience by dealing with errors quietly and gracefully. Nevertheless, it does not replace the need for good design, rigid code review, and reliable test procedures. Only by a combination of these four steps can you improve the overall quality of your software product.

Recommended Reading

For more information, see the following articles from Apple Developer Connection:

Jose Cruz has 10 years of experiences as a software engineer. He also writes articles for REALbasic Developer Magazine, MacTech, and Dr Dobbs Journal.


Return to MacDevCenter.com.

Copyright © 2009 O'Reilly Media, Inc.