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.
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 ofNSString
that uniquely identifies the exception. -
reason
- An instance ofNSString
containing a human-readable description of the exception. -
userInfo
- An instance ofNSDictionary
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.
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
- AnNSInteger
that represents the error's unique identifier. -
domain
- An instance ofNSString
defining the domain for the error (described in more detail in the next section). -
userInfo
- An instance ofNSDictionary
that contains application-specific information related to the error. This is typically used much more than theuserInfo
dictionary ofNSException
.
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
- AnNSString
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
- AnNSString
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
- AnNSString
instructing the user how to recover from the error. -
localizedRecoveryOptions
- AnNSArray
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
- AnNSString
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:
- Declare an
NSError
variable. You don't need to allocate or initialize it. - 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.
- 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.
Comments