Advertisement
iOS SDK

Objective-C Succinctly: Exceptions and Errors

by

In Objective-C, there are two types of errors that can occur while a program is running. Unexpected errors are "serious" programming errors that typically cause your program to exit prematurely. These are called exceptions, since they represent an exceptional condition in your program. On the other hand, expected errors occur naturally in the course of a program's execution and can be used to determine the success of an operation. These are referred to as errors.

You can also approach the distinction between exceptions and errors as a difference in their target audiences. In general, exceptions are used to inform the programmer about something that went wrong, while errors are used to inform the user that a requested action could not be completed.

Figure 34 Control flow for exceptions and errors

Control flow for exceptions and errors

For example, trying to access an array index that doesn't exist is an exception (a programmer error), while failing to open a file is an error (a user error). In the former case, something likely went very wrong in the flow of your program and it should probably shut down soon after the exception. In the latter, you would want to tell the user that the file couldn't be opened and possibly ask to retry the action, but there is no reason your program wouldn't be able to keep running after the error.


Exception Handling

The main benefit to Objective-C's exception handling capabilities is the ability to separate the handling of errors from the detection of errors. When a portion of code encounters an exception, it can "throw" it to the nearest error handling block, which can "catch" specific exceptions and handle them appropriately. The fact that exceptions can be thrown from arbitrary locations eliminates the need to constantly check for success or failure messages from each function involved in a particular task.

The @try, @catch(), and @finally compiler directives are used to catch and handle exceptions, and the @throw directive is used to detect them. If you've worked with exceptions in C#, these exception handling constructs should be familiar to you.

It's important to note that in Objective-C, exceptions are relatively slow. As a result, their use should be limited to catching serious programming errors-not for basic control flow. If you're trying to determine what to do based on an expected error (e.g., failing to load a file), please refer to the Error Handling section.

The NSException Class

Exceptions are represented as instances of the NSException class or a subclass thereof. This is a convenient way to encapsulate all the necessary information associated with an exception. The three properties that constitute an exception are described as follows:

  • name - An instance of NSString that uniquely identifies the exception.
  • reason - An instance of NSString containing a human-readable description of the exception.
  • userInfo - An instance of NSDictionary that contains application-specific information related to the exception.

The Foundation framework defines several constants that define the "standard" exception names. These strings can be used to check what type of exception was caught.

You can also use the initWithName:reason:userInfo: initialization method to create new exception objects with your own values. Custom exception objects can be caught and thrown using the same methods covered in the upcoming sections.

Generating Exceptions

Let's start by taking a look at the default exception-handling behavior of a program. The objectAtIndex: method of NSArray is defined to throw an NSRangeException (a subclass of NSException) when you try to access an index that doesn't exist. So, if you request the 10th item of an array that has only three elements, you'll have yourself an exception to experiment with:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
	@autoreleasepool {

		NSArray *crew = [NSArray arrayWithObjects:
						 @"Dave",
						 @"Heywood",
						 @"Frank", nil];

		// This will throw an exception.
		NSLog(@"%@", [crew objectAtIndex:10]);

	}
	return 0;
}

When it encounters an uncaught exception, Xcode halts the program and points you to the line that caused the problem.

Figure 35 Aborting a program due to an uncaught exception

Aborting a program due to an uncaught exception

Next, we'll learn how to catch exceptions and prevent the program from terminating.

Catching Exceptions

To handle an exception, any code that may result in an exception should be placed in a @try block. Then, you can catch specific exceptions using the @catch() directive. If you need to execute any housekeeping code, you can optionally place it in a @finally block. The following example shows all three of these exception-handling directives:

@try {
	NSLog(@"%@", [crew objectAtIndex:10]);
}
@catch (NSException *exception) {
	NSLog(@"Caught an exception");
	// We'll just silently ignore the exception.
}
@finally {
	NSLog(@"Cleaning up");
}

This should output the following in your Xcode console:

Caught an exception!
Name: NSRangeException
Reason: *** -[__NSArrayI objectAtIndex:]: index 10 beyond bounds [0 .. 2]
Cleaning up

When the program encounters the [crew objectAtIndex:10] message, it throws an NSRangeException, which is caught in the @catch() directive. Inside of the @catch() block is where the exception is actually handled. In this case, we just display a descriptive error message, but in most cases, you'll probably want to write some code to take care of the problem.

When an exception is encountered in the @try block, the program jumps to the corresponding @catch() block, which means any code after the exception occurred won't be executed. This poses a problem if the @try block needs some cleaning up (e.g., if it opened a file, that file needs to be closed). The @finally block solves this problem, since it is guaranteed to be executed regardless of whether an exception occurred. This makes it the perfect place to tie up any loose ends from the @try block.

The parentheses after the @catch() directive let you define what type of exception you're trying to catch. In this case, it's an NSException, which is the standard exception class. But, an exception can actually be any class-not just an NSException. For example, the following @catch() directive will handle a generic object:

@catch (id genericException)

We'll learn how to throw instances of NSException as well as generic objects in the next section.

Throwing Exceptions

When you detect an exceptional condition in your code, you create an instance of NSException and populate it with the relevant information. Then, you throw it using the aptly named @throw directive, prompting the nearest @try/@catch block to handle it.

For example, the following example defines a function for generating random numbers between a specified interval. If the caller passes an invalid interval, the function throws a custom error.

#import <Foundation/Foundation.h>

int generateRandomInteger(int minimum, int maximum) {
	if (minimum >= maximum) {
		// Create the exception.
		NSException *exception = [NSException
			exceptionWithName:@"RandomNumberIntervalException"
			reason:@"*** generateRandomInteger(): "
					"maximum parameter not greater than minimum parameter"
			userInfo:nil];

		// Throw the exception.
		@throw exception;
	}
	// Return a random integer.
	return arc4random_uniform((maximum - minimum) + 1) + minimum;
}

int main(int argc, const char * argv[]) {
	@autoreleasepool {

		int result = 0;
		@try {
			result = generateRandomInteger(0, 10);
		}
		@catch (NSException *exception) {
			NSLog(@"Problem!!! Caught exception: %@", [exception name]);
		}

		NSLog(@"Random Number: %i", result);

	}
	return 0;
}

Since this code passes a valid interval (0, 10) to generateRandomInteger(), it won't have an exception to catch. However, if you change the interval to something like (0, -10), you'll get to see the @catch() block in action. This is essentially what's going on under the hood when the framework classes encounter exceptions (e.g., the NSRangeException raised by NSArray).

It's also possible to re-throw exceptions that you've already caught. This is useful if you want to be informed that a particular exception occurred but don't necessarily want to handle it yourself. As a convenience, you can even omit the argument to the @throw directive:

@try {
	result = generateRandomInteger(0, -10);
}
@catch (NSException *exception) {
	NSLog(@"Problem!!! Caught exception: %@", [exception name]);

	// Re-throw the current exception.
	@throw
}

This passes the caught exception up to the next-highest handler, which in this case is the top-level exception handler. This should display the output from our @catch() block, as well as the default Terminating app due to uncaught exception... message, followed by an abrupt exit.

The @throw directive isn't limited to NSException objects-it can throw literally any object. The following example throws an NSNumber object instead of a normal exception. Also notice how you can target different objects by adding multiple @catch() statements after the @try block:

#import <Foundation/Foundation.h>

int generateRandomInteger(int minimum, int maximum) {
	if (minimum >= maximum) {
		// Generate a number using "default" interval.
		NSNumber *guess = [NSNumber
						   numberWithInt:generateRandomInteger(0, 10)];

		// Throw the number.
		@throw guess;
	}
	// Return a random integer.
	return arc4random_uniform((maximum - minimum) + 1) + minimum;
}

int main(int argc, const char * argv[]) {
	@autoreleasepool {

		int result = 0;
		@try {
			result = generateRandomInteger(30, 10);
		}
		@catch (NSNumber *guess) {
			NSLog(@"Warning: Used default interval");
			result = [guess intValue];
		}
		@catch (NSException *exception) {
			NSLog(@"Problem!!! Caught exception: %@", [exception name]);
		}

		NSLog(@"Random Number: %i", result);

	}
	return 0;
}

Instead of throwing an NSException object, generateRandomInteger() tries to generate a new number between some "default" bounds. The example shows you how @throw can work with different types of objects, but strictly speaking, this isn't the best application design, nor is it the most efficient use of Objective-C's exception-handling tools. If you really were just planning on using the thrown value like the previous code does, you would be better off with a plain old conditional check using NSError, as discussed in the next section.

In addition, some of Apple's core frameworks expect an NSException object to be thrown, so be careful with custom objects when integrating with the standard libraries.


Error Handling

Whereas exceptions are designed to let programmers know when things have gone fatally wrong, errors are designed to be an efficient, straightforward way to check if an action succeeded or not. Unlike exceptions, errors are designed to be used in your everyday control flow statements.

The NSError Class

The one thing that errors and exceptions have in common is that they are both implemented as objects. The NSError class encapsulates all of the necessary information for representing errors:

  • code - An NSInteger that represents the error's unique identifier.
  • domain - An instance of NSString defining the domain for the error (described in more detail in the next section).
  • userInfo - An instance of NSDictionary that contains application-specific information related to the error. This is typically used much more than the userInfo dictionary of NSException.

In addition to these core attributes, NSError also stores several values designed to aid in the rendering and processing of errors. All of these are actually shortcuts into the userInfo dictionary described in the previous list.

  • localizedDescription - An NSString containing the full description of the error, which typically includes the reason for the failure. This value is typically displayed to the user in an alert panel.
  • localizedFailureReason - An NSString containing a stand-alone description of the reason for the error. This is only used by clients that want to isolate the reason for the error from its full description.
  • recoverySuggestion - An NSString instructing the user how to recover from the error.
  • localizedRecoveryOptions - An NSArray of titles used for the buttons of the error dialog. If this array is empty, a single OK button is displayed to dismiss the alert.
  • helpAnchor - An NSString to display when the user presses the Help anchor button in an alert panel.

As with NSException, the initWithDomain:code:userInfo method can be used to initialize custom NSError instances.

Error Domains

An error domain is like a namespace for error codes. Codes should be unique within a single domain, but they can overlap with codes from other domains. In addition to preventing code collisions, domains also provide information about where the error is coming from. The four main built-in error domains are: NSMachErrorDomain, NSPOSIXErrorDomain, NSOSStatusErrorDomain, and NSCocoaErrorDomain. The NSCocoaErrorDomain contains the error codes for many of Apple's standard Objective-C frameworks; however, there are some frameworks that define their own domains (e.g., NSXMLParserErrorDomain).

If you need to create custom error codes for your libraries and applications, you should always add them to your own error domain-never extend any of the built-in domains. Creating your own domain is a relatively trivial job. Because domains are just strings, all you have to do is define a string constant that doesn't conflict with any of the other error domains in your application. Apple suggests that domains take the form of com.<company>.<project>.ErrorDomain.

Capturing Errors

There are no dedicated language constructs for handling NSError instances (though several built-in classes are designed to handle them). They are designed to be used in conjunction with specially designed functions that return an object when they succeed and nil when they fail. The general procedure for capturing errors is as follows:

  1. Declare an NSError variable. You don't need to allocate or initialize it.
  2. Pass that variable as a double pointer to a function that may result in an error. If anything goes wrong, the function will use this reference to record information about the error.
  3. Check the return value of that function for success or failure. If the operation failed, you can use NSError to handle the error yourself or display it to the user.

As you can see, a function doesn't typically return an NSError object-it returns whatever value it's supposed to if it succeeds, otherwise it returns nil. You should always use the return value of a function to detect errors-never use the presence or absence of an NSError object to check if an action succeeded. Error objects are only supposed to describe a potential error, not tell you if one occurred.

The following example demonstrates a realistic use case for NSError. It uses a file-loading method of NSString, which is actually outside the scope of the book. The iOS Succinctly book covers file management in depth, but for now, let's just focus on the error-handling capabilities of Objective-C.

First, we generate a file path pointing to ~/Desktop/SomeContent.txt. Then, we create an NSError reference and pass it to the stringWithContentsOfFile:encoding:error: method to capture information about any errors that occur while loading the file. Note that we're passing a reference to the *error pointer, which means the method is requesting a pointer to a pointer (i.e. a double pointer). This makes it possible for the method to populate the variable with its own content. Finally, we check the return value (not the existence of the error variable) to see if stringWithContentsOfFile:encoding:error: succeeded or not. If it did, it's safe to work with the value stored in the content variable; otherwise, we use the error variable to display information about what went wrong.

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
	@autoreleasepool {

		// Generate the desired file path.
		NSString *filename = @"SomeContent.txt";
		NSArray *paths = NSSearchPathForDirectoriesInDomains(
							 NSDesktopDirectory, NSUserDomainMask, YES
						 );
		NSString *desktopDir = [paths objectAtIndex:0];
		NSString *path = [desktopDir
						  stringByAppendingPathComponent:filename];

		// Try to load the file.
		NSError *error;
		NSString *content = [NSString stringWithContentsOfFile:path
							 encoding:NSUTF8StringEncoding
							 error:&error];

		// Check if it worked.
		if (content == nil) {
			// Some kind of error occurred.
			NSLog(@"Error loading file %@!", path);
			NSLog(@"Description: %@", [error localizedDescription]);
			NSLog(@"Reason: %@", [error localizedFailureReason]);
		} else {
			// Content loaded successfully.
			NSLog(@"Content loaded!");
			NSLog(@"%@", content);
		}
	}
	return 0;
}

Since the ~/Desktop/SomeContent.txt file probably doesn't exist on your machine, this code will most likely result in an error. All you have to do to make the load succeed is create SomeContent.txt on your desktop.

Custom Errors

Custom errors can be configured by accepting a double pointer to an NSError object and populating it on your own. Remember that your function or method should return either an object or nil, depending on whether it succeeds or fails (do not return the NSError reference).

The next example uses an error instead of an exception to mitigate invalid parameters in the generateRandomInteger() function. Notice that **error is a double pointer, which lets us populate the underlying variable from within the function. It's very important to check that the user actually passed a valid **error parameter with if (error != NULL). You should always do this in your own error-generating functions. Since the **error parameter is a double pointer, we can assign a value to the underlying variable via *error. And again, we check for errors using the return value (if (result == nil)), not the error variable.

#import <Foundation/Foundation.h>

NSNumber *generateRandomInteger(int minimum, int maximum, NSError **error) {
	if (minimum >= maximum) {
		if (error != NULL) {

			// Create the error.
			NSString *domain = @"com.MyCompany.RandomProject.ErrorDomain";
			int errorCode = 4;
			NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
			[userInfo setObject:@"Maximum parameter is not greater than minimum parameter"
						 forKey:NSLocalizedDescriptionKey];

			// Populate the error reference.
			*error = [[NSError alloc] initWithDomain:domain
												code:errorCode
											userInfo:userInfo];
		}
		return nil;
	}
	// Return a random integer.
	return [NSNumber
			numberWithInt:arc4random_uniform((maximum - minimum) + 1) + minimum];
}

int main(int argc, const char * argv[]) {
	@autoreleasepool {

		NSError *error;
		NSNumber *result = generateRandomInteger(0, -10, &error);

		if (result == nil) {
			// Check to see what went wrong.
			NSLog(@"An error occurred!");
			NSLog(@"Domain: %@ Code: %li", [error domain], [error code]);
			NSLog(@"Description: %@", [error localizedDescription]);
		} else {
			// Safe to use the returned value.
			NSLog(@"Random Number: %i", [result intValue]);
		}

	}
	return 0;
}

All of the localizedDescription, localizedFailureReason, and related properties of NSError are actually stored in its userInfo dictionary using special keys defined by NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey, etc. So, all we have to do to describe the error is add some strings to the appropriate keys, as shown in the last sample.

Typically, you'll want to define constants for custom error domains and codes so that they are consistent across classes.


Summary

This chapter provided a detailed discussion of the differences between exceptions and errors. Exceptions are designed to inform programmers of fatal problems in their program, whereas errors represent a failed user action. Generally, a production-ready application should not throw exceptions, except in the case of truly exceptional circumstances (e.g., running out of memory in a device).

We covered the basic usage of NSError, but keep in mind that there are several built-in classes dedicated to processing and displaying errors. Unfortunately, these are all graphical components, and thus outside the scope of this book. The iOS Succinctly sequel has a dedicated section on displaying and recovering from errors.

In the final chapter of Objective-C Succinctly, we'll discuss one of the more confusing topics in Objective-C. We'll discover how blocks let us treat functionality the same way we treat data. This will have a far-reaching impact on what's possible in an Objective-C application.

This lesson represents a chapter from Objective-C Succinctly, a free eBook from the team at Syncfusion.
Related Posts