Why Objective-C Has try-catch But Nobody Uses It
If you've written Objective-C, you've seen the NSError ** pattern everywhere. Methods return nil or NO for failure and populate an error pointer with details. But Objective-C also has @try, @catch, and @throw—a complete exception system. So why does virtually no one use it?
The Exception Syntax
Objective-C exceptions work like you'd expect:
@try {
[self riskyOperation];
}
@catch (NSException *exception) {
NSLog(@"Caught: %@", exception.reason);
}
@finally {
[self cleanup];
}
You can throw exceptions yourself:
- (void)validateInput:(NSString *)input {
if (input.length == 0) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"Input cannot be empty"
userInfo:nil];
}
}
Looks reasonable. So what's the problem?
Exceptions Aren't Memory Safe
Here's the killer issue. Consider this code under Manual Reference Counting:
- (void)processFile:(NSString *)path {
NSFileHandle *handle = [[NSFileHandle alloc] initWithPath:path];
[self doSomethingThatMightThrow]; // If this throws...
[handle release]; // This never runs
}
When an exception throws, execution jumps directly to the catch block. The release call never happens. Memory leaks.
"But we have ARC now!" True, but ARC doesn't help with non-object resources:
- (void)processData {
void *buffer = malloc(1024);
[self doSomethingThatMightThrow];
free(buffer); // Leaked if exception thrown
}
ARC manages Objective-C objects, not C memory, file handles, locks, or other resources. Exceptions bypass all cleanup code between the throw site and catch block.
The -fobjc-arc-exceptions Flag
Clang has a flag that makes ARC exception-safe: -fobjc-arc-exceptions. It inserts cleanup code at every possible throw point. But this has a cost—significant code size increase and slower execution, even when no exceptions occur.
Apple ships their frameworks without this flag. If you throw an exception through Apple's code, you'll leak. This isn't a theoretical concern; it's how Foundation and UIKit are compiled.
NSError: The Alternative
The NSError ** pattern sidesteps these issues entirely:
- (BOOL)processFile:(NSString *)path error:(NSError **)error {
NSFileHandle *handle = [[NSFileHandle alloc] initWithPath:path];
if (!handle) {
if (error) {
*error = [NSError errorWithDomain:@"FileError"
code:1
userInfo:@{NSLocalizedDescriptionKey: @"Cannot open file"}];
}
return NO;
}
BOOL success = [self processHandle:handle error:error];
[handle closeFile];
return success;
}
No abrupt control flow. Every code path executes normally. Resources always get cleaned up because the method returns normally—just with a failure value.
The Pattern in Practice
The calling convention is consistent across all of Apple's frameworks:
NSError *error;
NSData *data = [NSData dataWithContentsOfFile:path options:0 error:&error];
if (!data) {
NSLog(@"Failed: %@", error.localizedDescription);
return;
}
Check the return value first, then examine the error if the operation failed. Never check error after a successful operation—it might contain garbage from a previous failed attempt in the same block.
When Exceptions Are Appropriate
Apple's documentation is clear: exceptions are for programmer errors, not runtime conditions. Use NSException for:
- Logic errors that should never happen in correct code
- Violated preconditions that indicate bugs
- Assertions that should crash during development
- (void)setName:(NSString *)name {
NSParameterAssert(name != nil); // Throws if nil in debug
_name = name;
}
These exceptions aren't meant to be caught. They signal "stop everything, there's a bug." The app should crash so developers notice during testing.
The Unwritten Contract
Objective-C frameworks follow an unwritten contract: methods don't throw exceptions during normal operation. If you call a Foundation method correctly, it won't throw. It might fail and return nil with an error, but it won't throw.
This means you generally don't need @try blocks around system API calls. The only exceptions come from programmer error—out-of-bounds array access, invalid arguments, that sort of thing.
Bridging to Swift
When Apple designed Swift, they acknowledged this reality. Swift errors aren't exceptions—they're explicit return values with syntactic sugar. try/catch in Swift compiles to the same pattern as NSError pointers, just with nicer syntax.
// Swift
do {
let data = try Data(contentsOf: url)
} catch {
print(error)
}
// Compiles to something like the NSError ** pattern
The Objective-C exception system exists and works, but the community learned decades ago that it doesn't fit Objective-C's memory model. NSError won because it's boring, predictable, and safe.