Why is raising an NSException not bringing down my application?
Asked Answered
C

6

13

The Problem

I'm writing a Cocoa application and I want to raise exceptions that will crash the application noisily.

I have the following lines in my application delegate:

[NSException raise:NSInternalInconsistencyException format:@"This should crash the application."];
abort();

The problem is, they don't bring down the application - the message is just logged to the console and the app carries on it's merry way.

As I understand it, the whole point of exceptions is that they're fired under exceptional circumstances. In these circumstances, I want the application to quit in an obvious way. And this doesn't happen.

What I've tried

I've tried:

-(void)applicationDidFinishLaunching:(NSNotification *)note
    // ...
    [self performSelectorOnMainThread:@selector(crash) withObject:nil waitUntilDone:YES];
}

-(void)crash {
    [NSException raise:NSInternalInconsistencyException format:@"This should crash the application."];
    abort();
}

which doesn't work and

-(void)applicationDidFinishLaunching:(NSNotification *)note
    // ...
    [self performSelectorInBackground:@selector(crash) withObject:nil];
}

-(void)crash {
    [NSException raise:NSInternalInconsistencyException format:@"This should crash the application."];
    abort();
}

which, rather confusingly, works as expected.

What's going on? What am I doing wrong?

Change answered 26/7, 2010 at 15:22 Comment(0)
M
10

UPDATE - Nov 16, 2010: There are some issues with this answer when exceptions are thrown inside IBAction methods. See this answer instead:

How can I stop HIToolbox from catching my exceptions?


This expands on David Gelhar's answer, and the link he provided. Below is how I did it by overriding NSApplication's -reportException: method. First, create an ExceptionHandling Category for NSApplication (FYI, you should add a 2-3 letter acronym before "ExceptionHandling" to reduce the risk of name clashing):

NSApplication+ExceptionHandling.h

#import <Cocoa/Cocoa.h>

@interface NSApplication (ExceptionHandling)

- (void)reportException:(NSException *)anException;

@end

NSApplication+ExceptionHandling.m

#import "NSApplication+ExceptionHandling.h"

@implementation NSApplication (ExceptionHandling)

- (void)reportException:(NSException *)anException
{
    (*NSGetUncaughtExceptionHandler())(anException);
}

@end

Second, inside NSApplication's delegate, I did the following:

AppDelegate.m

void exceptionHandler(NSException *anException)
{
    NSLog(@"%@", [anException reason]);
    NSLog(@"%@", [anException userInfo]);

    [NSApp terminate:nil];  // you can call exit() instead if desired
}

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
    NSSetUncaughtExceptionHandler(&exceptionHandler);

    // additional code...

    // NOTE: See the "UPDATE" at the end of this post regarding a possible glitch here...
}

Rather than use NSApp's terminate:, you can call exit() instead. terminate: is more Cocoa-kosher, though you may want to skip your applicationShouldTerminate: code in the event an exception was thrown and simply hard-crash with exit():

#import "sysexits.h"

// ...

exit(EX_SOFTWARE);

Whenever an exception is thrown, on the main thread, and it's not caught and destroyed, your custom uncaught exception handler will now be called instead of NSApplication's. This allows you to crash your application, among other things.


UPDATE:

There appears to be a small glitch in the above code. Your custom exception handler won't "kick in" and work until after NSApplication has finished calling all of its delegate methods. This means that if you do some setup-code inside applicationWillFinishLaunching: or applicationDidFinishLaunching: or awakeFromNib:, the default NSApplication exception handler appears to be in-play until after it's fully initialized.

What that means is if you do this:

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
        NSSetUncaughtExceptionHandler(&exceptionHandler);

        MyClass *myClass = [[MyClass alloc] init];   // throws an exception during init...
}

Your exceptionHandler won't get the exception. NSApplication will, and it'll just log it.

To fix this, simply put any initialization code inside a @try/@catch/@finally block and you can call your custom exceptionHandler:

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
    NSSetUncaughtExceptionHandler(&exceptionHandler);

    @try
    {
        MyClass *myClass = [[MyClass alloc] init];   // throws an exception during init...
    }
    @catch (NSException * e)
    {
        exceptionHandler(e);
    }
    @finally
    {
        // cleanup code...
    }
}

Now your exceptionHandler() gets the exception and can handle it accordingly. After NSApplication has finished calling all delegate methods, the NSApplication+ExceptionHandling.h Category kicks in, calling exceptionHandler() through its custom -reportException: method. At this point you don't have to worry about @try/@catch/@finally when you want exceptions to raise to your Uncaught Exception Handler.

I'm a little baffled by what is causing this. Probably something behind-the-scenes in the API. It occurs even when I subclass NSApplication, rather than adding a category. There may be other caveats attached to this as well.

Miley answered 5/8, 2010 at 20:54 Comment(1)
This solution is overly complex. George's answer below is the right way to do it: "[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];"Tenderize
A
8

There turns out to be a very simple solution:

[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];

It does not crash your app if you use @try ... @catch.

I can't begin to imagine why this isn't the default.

Archivist answered 23/9, 2016 at 3:49 Comment(2)
Note that this doesn't kick in until after NSApplication has finished calling all of its delegate methods.Eleen
Actually it's worse than that. It doesn't work in any AppleEvent handling code. See answer below for workaround.Eleen
R
3

Maybe you can use NSSetUncaughtExceptionHandler, or create a category on NSApplication that overrides -reportException:, as suggested at http://www.cocoadev.com/index.pl?StackTraces

Remittent answered 26/7, 2010 at 15:51 Comment(1)
Excellent suggestion, David. I read this page lots a few months ago, but didn't try the NSApplication category override for some reason. I'll have a go at doing it this way as it's much easier than trying to get all my code running on background threads!Change
C
2

I've posted this question and answer as I wish someone had told me this, oh, about a year ago:

Exceptions thrown on the main thread are caught by NSApplication.

I skim read the docs on NSException end to end, with no mention of this that I can recall. The only reason I know this is because of the fantastic Cocoa Dev:

http://www.cocoadev.com/index.pl?ExceptionHandling

The Solution. I guess.

I've got a daemon with no UI that almost entirely runs on the main thread. I'll have to transfer the whole app to run background threads, unless someone else can suggest a way of stopping NSApplication catching just the exceptions I throw. I'm pretty sure that's not possible.

Change answered 26/7, 2010 at 15:30 Comment(4)
I think you missed a page. developer.apple.com/mac/library/documentation/Cocoa/Conceptual/… "Note: Exceptions on the main thread of a Cocoa application do not typically rise to the level of the uncaught exception handler because the global application object catches all such exceptions." ... the main body of the page also mentions the solution David Gelhar spoke of.Metonymy
Yep, obviously very lazy reading on my part. :) Thanks for pointing this out. There's even a box round it to highlight it. Duh.Change
Hi John, I posted an "answer" below in an attempt to understand the issue more clearly. Any ideas?Municipalize
Never mind, I think I found a solution to my issue. I've updated my "answer" accordingly.Municipalize
M
1

I'm trying to understand this properly: Why does the following category method on NSApplication lead to an infinite loop? In that infinite loop, "An uncaught exception was raised" is logged out infinitely many times:

- (void)reportException:(NSException *)anException
{
    // handle the exception properly
    (*NSGetUncaughtExceptionHandler())(anException);
}

For testing (and understanding purposes), this is the only thing I do, i.e. just create the above category method. (According to the instructions in http://www.cocoadev.com/index.pl?StackTraces)

Why would this cause an infinite loop? It's not consistent with what the default uncaught exception handler method should do, i.e. just log the exception and exit the program. (See http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Exceptions/Concepts/UncaughtExceptions.html#//apple_ref/doc/uid/20000056-BAJDDGGD)

Could it be that the default uncaught exception handler is actually throwing the exception again, leading to this infinite loop?

Note: I know it's silly to create only this category method. The purpose of this is to gain a better understanding.

UPDATE: Never mind, I think i get this now. Here is my take. By default, as we know, NSApplication's reportException: method logs the exception. But, according to the docs, the default uncaught exception handler logs the exception and exists the program. However, this should be worded like this in the docs to be more precise: The default uncaught exception handler calls NSApplication's reportException: method (in order to log it, which the method's default implementation indeed does), and then exists the program. So now it should be clear why calling the default uncaught exception handler inside an overridden reportException: causes an infinite loop: The former calls the latter.

Municipalize answered 2/6, 2011 at 10:50 Comment(0)
E
1

So it turns out the reason that it appears that the exception handler doesn't get called in your application delegate methods is that _NSAppleEventManagerGenericHandler (a private API) has a @try @catch block that is catching all exceptions and just calling NSLog on them before returning with a errAEEventNotHandled OSErr. This means that not only are you going to miss any exceptions in app start up, but essentially any exceptions that occur inside of handling an AppleEvent which includes (but is not limited to) opening documents, printing, quitting and any AppleScript.

So, my "fix" for this:

#import <Foundation/Foundation.h>
#include <objc/runtime.h>

@interface NSAppleEventManager (GTMExceptionHandler)
@end

@implementation NSAppleEventManager (GTMExceptionHandler)
+ (void)load {
  // Magic Keyword for turning on crashes on Exceptions
  [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];

  // Default AppleEventManager wraps all AppleEvent calls in a @try/@catch
  // block and just logs the exception. We replace the caller with a version
  // that calls through to the NSUncaughtExceptionHandler if set.
  NSAppleEventManager *mgr = [NSAppleEventManager sharedAppleEventManager];
  Class class = [mgr class];
  Method originalMethod = class_getInstanceMethod(class, @selector(dispatchRawAppleEvent:withRawReply:handlerRefCon:));
  Method swizzledMethod = class_getInstanceMethod(class, @selector(gtm_dispatchRawAppleEvent:withRawReply:handlerRefCon:));
  method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (OSErr)gtm_dispatchRawAppleEvent:(const AppleEvent *)theAppleEvent
                      withRawReply:(AppleEvent *)theReply
                     handlerRefCon:(SRefCon)handlerRefCon {
  OSErr err;
  @try {
    err = [self gtm_dispatchRawAppleEvent:theAppleEvent withRawReply:theReply handlerRefCon:handlerRefCon];
  } @catch(NSException *exception) {
    NSUncaughtExceptionHandler *handler = NSGetUncaughtExceptionHandler();
    if (handler) {
      handler(exception);
    }
    @throw;
  }
  @catch(...) {
    @throw;
  }
  return err;
}
@end

Fun extra note: NSLog(@"%@", exception) is equivalent to NSLog(@"%@", exception.reason). NSLog(@"%@", [exception debugDescription]) will give you the reason plus the fully symbolicated stack backtrace.

The default version in the _NSAppleEventManagerGenericHandler just calls NSLog(@"%@", exception) (macOS 10.14.4 (18E226))

Eleen answered 17/5, 2019 at 22:55 Comment(2)
Filed radar 50933952 - [NSAppleEventManager] Please do better exception logging and radar 50933868 - NSAppleEventManager should respect exception handling settingsEleen
I should also note that my fix above will change the way that AppleEvents interact with your app, but only in the case when an exception is thrown. Without the fix, your app will return an errAppleEventNotHandled, and will continue to attempt to limp along, potentially in a corrupted state. With my fix, the app will crash, and whomever called you will get a connectionInvalid err.Eleen

© 2022 - 2024 — McMap. All rights reserved.