Articles index

NSAssert considered harmless

November 8 2019 by Jeff Johnson
To support this blog please buy my apps StopTheMadness and Underpass

My friend Ilja Iwas had a question: if a Mac app enables the ENABLE_NS_ASSERTIONS build setting, why doesn't the app crash when it calls NSAssert in the applicationDidFinishLaunching: method of the NSApplicationDelegate protocol? Good question! Curiously, the app does crash when it calls NSAssert in the applicationWillFinishLaunching: method, so we have a bit of a mystery.

The header file NSException.h in the Foundation framework contains the definition of NSAssert:

#define NSAssert(condition, desc, ...)	\
    do {				\
	__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
	if (__builtin_expect(!(condition), 0)) {		\
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
	    [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
		object:self file:__assert_file__ \
	    	lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
	}				\
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)

Looking at the Foundation framework disassembly ($ otool -tV /System/Library/Frameworks/Foundation.framework/Foundation), we can see that -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] calls +[NSException raise:format:arguments:] with the exception name NSInternalInconsistencyException. In turn, +[NSException raise:format:arguments:] calls the objc_exception_throw function of the Objective-C runtime. If we set a breakpoint on objc_exception_throw, we can see that it does indeed get hit. So why doesn't the app crash?

Set another breakpoint on the objc_begin_catch function. This breakpoint gets hit too! Now we have our answer. The exception is caught in the -[NSAppleEventManager dispatchRawAppleEvent:withRawReply:handlerRefCon:] method of the AppKit framework. If we look at the backtrace, we can see that this method calls -[NSApplication(NSAppleEventHandling) _handleAEOpenEvent:], which posts an NSApplicationDidFinishLaunchingNotification notification observed by the NSApplication delegate. The key point is that when NSAppleEventManager dispatches an Apple Event, it wraps the code in an Objective-C @try{}@catch{} block. Thus, if applicationDidFinishLaunching: throws an exception, the exception is harmlessly caught by NSAppleEventManager instead of going uncaught and crashing the whole app.

It's important to note that even though NSAppleEventManager catches exceptions, an exception still interrupts the flow of code in the applicationDidFinishLaunching: method. It's still true that no code in the method following the NSAssert will be executed. You might say, then, that NSAppleEventManager renders NSAssert "mostly harmless".

As a consequence of NSAppleEventManager catching exceptions, NSAssert will not crash an app in any method triggered by an Apple Event. There are many NSApplicationDelegate methods triggered by Apple Events, such as applicationShouldHandleReopen:hasVisibleWindows: to give one example. If you want to assure that your app crashes on assertions, you need to use the standard UNIX function assert. Good old assert, nothing beats that!

To support this blog please buy my apps StopTheMadness and Underpass

Articles index