10

In Apple's Concurrency Programming Guide the NSOperation subclass examples (both non-concurrent and concurrent varieties) use exception handling and I'm wondering why they are encouraging this style within operations.

Listing 2-4 Responding to a cancellation request

- (void)main {
   @try {
      BOOL isDone = NO;

      while (![self isCancelled] && !isDone) {
          // Do some work and set isDone to YES when finished
      }
   }
   @catch(...) {
      // Do not rethrow exceptions.
   }
}

My understanding is that generally exception handling is not a common practice in Objective-C code - exceptions are essentially programmer errors and should cause the app to crash whereas unexpected inputs are best handled by NSError. (My possibly misinformed understanding comes from things like this and this)

I'm wondering if NSOperations present a particular situation in which exception handling is important, or if this is more the preferred style of the particular author of that guide.

As a side note, some of the NSOperation example code follows this style, other examples do not. Most high-visibility OSS does not use exceptions (AFNetworking, for example).

Community
  • 1
  • 1
edelaney05
  • 6,822
  • 6
  • 41
  • 65

2 Answers2

9

Your understanding is correct - NSError (or similar) should be used to convey error information, rather than exceptions. Most Objective-C code is not exception-safe and will at the very least leak resources. As a general rule, never let your code leak an exception into anyone else's code - whether Apple's or a 3rd parties. Some 3rd party frameworks may explicitly indicate they are exception safe, but it's rare.

By that principle you can see why you should have a catch-all exception handler in your main method regardless. But there's actually another reason: your operation will be run on a dedicated thread. Exceptions thrown from your operation will propagate up the stack, but no further. The logical caller or owner of the operation won't get them, as they're running on a different thread (or not at all). So leaked exceptions will either kill your whole program, or be swallowed silently with no other indication. Your program may then get stuck in a weird state - since you didn't realise an error occurred, you may continue waiting for the result of your operation that will never arrive.

Additionally, Apple has a section in the Concurrency Programming Guide where they talk about Handling Errors and Exceptions. Their first point on "discrete entities" is alluding to what I said in the previous paragraph:

Handling Errors and Exceptions

Because operations are essentially discrete entities inside your application, they are responsible for handling any errors or exceptions that arise. In OS X v10.6 and later, the default start method provided by the NSOperation class does not catch exceptions. (In OS X v10.5, the start method does catch and suppress exceptions.) Your own code should always catch and suppress exceptions directly. It should also check error codes and notify the appropriate parts of your application as needed. And if you replace the start method, you must similarly catch any exceptions in your custom implementation to prevent them from leaving the scope of the underlying thread.

Among the types of error situations you should be prepared to handle are the following:

  • Check and handle UNIX errno-style error codes.
  • Check explicit error codes returned by methods and functions.
  • Catch exceptions thrown by your own code or by other system frameworks.
  • Catch exceptions thrown by the NSOperation class itself, which throws exceptions in the following situations:
    • When the operation is not ready to execute but its start method is called
    • When the operation is executing or finished (possibly because it was canceled) and its start method is called again
    • When you try to add a completion block to an operation that is already executing or finished
    • When you try to retrieve the result of an NSInvocationOperation object that was canceled

If your custom code does encounter an exception or error, you should take whatever steps are needed to propagate that error to the rest of your application. The NSOperation class does not provide explicit methods for passing along error result codes or exceptions to other parts of your application. Therefore, if such information is important to your application, you must provide the necessary code.

Wade Tregaskis
  • 1,996
  • 11
  • 15
  • 1
    Perhaps I should have stated more explicitly that the exception handler in `main` is defensive programming: even if your code doesn't raise exceptions deliberately, having the handler gives you the piece of mind that no matter what code you end up calling, or what happens in future, you'll be safe. It's the same reason you should always wrap lock/unlock functions in exception handlers, to ensure you don't leave locks locked and end up deadlocking (or just use @synchronize, in the specific case of locks, which does that for you). – Wade Tregaskis Dec 02 '12 at 21:09
  • Wade, I've been noodling on this for the past couple of days, and still am having trouble getting my head wrapped around your answer. If I have a programming error (e.g. divide by zero), I want my thread (and therefore app, right?) to crash. Specifically, I do not understand the following: "Most Objective-C code is not exception-safe and will at the very least leak resources" (you mean leak memory after throwing an exception? How?) and from Apple's guide "Your own code should always catch and suppress exceptions directly" (again, isn't the point of exceptions to crash?) – edelaney05 Dec 04 '12 at 21:29
  • 1
    Well, Apple's wording is misleading. You shouldn't normally "suppress" exceptions, except in very specific circumstances where you know for certain they're harmless. If you want your app to abort on any exception, you should still write the exception handler. It'll just contain some logging, perhaps (you could use `-[NSApp reportException:]`), and then kill your app - perhaps by calling abort(). "Normally" this is done by the default unhandled exception handler, but you can't rely on the exception passing through NSOperation's code to that - it explicitly tells you not to. – Wade Tregaskis Dec 05 '12 at 00:36
  • Hm. Okay, that's starting to make more sense. Instead of calling abort() can I raise the exception over on the main thread? in the @catch block would something like `[exception performSelectorOnMainThread:@selector(raise) ...]` work? (The motivation being unit testing) – edelaney05 Dec 06 '12 at 16:52
3

I think this post and the accompanying answer elaborates very well on the general exception- vs. no exception handling topic!

It is unsafe to throw exceptions in circumstances where resources are not automatically managed. This is the case of the Cocoa framework (and neighbor frameworks), as they use manual reference counting.

If you throw an exception, any release call you skip over by unwinding the stack will result in a leak. This should limit you tothrowing only if you're certain that you're not going to recover since all resources are returned to the OS when a process quits.

Unfortunately, NSRunLoops tend to catch all exceptions that propagate to them, so if you throw during an event, you'll resume to the next event. This is, obviously, very bad. Therefore, it's better that you simply don't throw.

This problem is diminished if you use garbage-collected Objective-C, as any resource represented by an Objective-C object will be properly released. However, C resources (such as file descriptors or malloc-allocated memory) that are not wrapped in an Objective-C object will still leak.

So, all in all, don't throw.

The Cocoa API has several workarounds to this, as you mentioned. Returning nil and the NSError** pattern are two of them.

Community
  • 1
  • 1
Nenad M
  • 3,055
  • 20
  • 26