Item 32: Beware of Memory Management with Exception-Safe Code
Exceptions are a language feature offered by many modern languages. Exceptions do not exist in pure C but do in both C++ and Objective-C. In fact, in the modern runtime, C++ and Objective-C exceptions are compatible, meaning that an exception thrown from one language can be caught using a handler from the other language.
Even though the error model of Objective-C (see Item 21) states that you should use exceptions only for fatal errors, you may still need code that catches and handles them. Examples are Objective-C++ code or code that interfaces with a third-party library such that you have no control over the exceptions being thrown. Also, some system libraries still make use of exceptions, harking back to the days when exceptions were in common use. For example, Key-Value Observing (KVO) will throw an exception if you attempt to unregister an observer that was not already registered.
When it comes to memory management, exceptions introduce an interesting problem. Inside atry
block, if an object is retained and then an exception is thrown before the object has been released, the object will leak unless this case is handled in the catch
block. C++ destructors are run by the Objective-C exception-handling routines. This is important for C++ because any object whose lifetime has been cut short by a thrown exception needs to be destructed; otherwise, the memory it uses will be leaked, not to mention all the other system resources, such as file handles, that may not be cleaned up properly.
The destruction of objects automatically by the exception-handling routines is something that is trickier to do in an environment of manual reference counting. Consider the following Objective-C code, which uses manual reference counting:
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
[object release];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
At first glance, this may seem correct. But what happens if doSomethingThatMayThrow
throws an exception? The release
on the following line would not be run, because the exception would halt the execution and jump to the catch
block. So in this scenario, the object would be leaked if an exception were thrown. That’s not ideal. The way this is solved is to use the @finally
block, which is guaranteed to be run once and only once, whether or not an exception is thrown. For example, the code could be transformed into this:
EOCSomeClass *object;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
@finally {
[object release];
}
Note how the object has had to be pulled out of the @try
block because it needs to be referenced in the @finally
block. This can get very tedious if you have to do this for all objects that need releasing. Also, if the logic is more complex than this, with multiple statements within the @try
block, it can be easy to overlook the scenario in which an object might potentially leak. If the object that is leaked is a scarce resource (or manages one), such as a file descriptor or database connection, the leak is potentially disastrous because eventually, the application could end up unnecessarily holding onto all of a system’s resources.
With ARC, the situation is even more serious. The equivalent ARC code for the original code is this:
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
Now it is even more of a problem; you can’t use the trick of putting the release in the @finally
block, as it is illegal to call release
. But surely ARC handles this situation, you are probably thinking. Well, by default, it does not; to do so requires a large amount of boilerplate code to be added to track the objects that potentially need cleaning up if an exception is thrown. This code can severely decrease performance at runtime even when no exceptions are thrown. This code also increases the size of an application significantly with all the extra code that has to be added. These side effects are not ideal.
Although it is not turned on by default, ARC does support emitting this extra code to handle exceptions safely. The code can be turned on by using the compiler flag -fobjc-arc-exceptions
. The rationale behind not turning it on by default is that in Objective-C programming, exceptions should be used only when the application will terminate as a result of that exception being thrown (see Item 21). Therefore, if the application is going to terminate anyway, the potential memory leak is irrelevant. There is no point adding the required code to become exception safe if the application is going to terminate.
The one scenario in which the -fobjc-arc-exceptions
flag is turned on by default occurs when the compiler is in Objective-C++ mode. C++ already needs to have code similar to the code that ARC would implement, so the performance hit is not as great if ARC adds in its own code to ensure exception safety. Also, C++ makes heavy use of exceptions, so it’s likely that a developer using Objective-C++ would want to use exceptions.
If you are using manual reference counting and must catch exceptions, remember to ensure that code is written in such a way to clean up correctly. If you are using ARC and must catch exceptions, you will need to turn on the -fobjc-arc-exceptions
flag. But above all, if you find yourself catching a lot of exceptions, consider refactoring to make use of NSError
-style error passing instead, as explained in Item 21.
Things to Remember
When exceptions are caught, care should be taken to ensure that any required cleanup is done for objects created within the try
block.
By default, ARC does not emit code that handles cleanup when exceptions are thrown. This can be enabled with a compiler flag but produces code that is larger and comes with a runtime cost.