C++ exceptions on iOS

I use Crashlytics to generate crash reports for an iOS app. When C++ exceptions are thrown on the main thread, they are caught by a CFRunLoop exception handler and rethrown, which destroys all the information about where in the code the original exception is thrown.


There is an open radar for this, opened two years ago, that was marked as "duplicate", but the duplicate doesn't exist as far as I can tell: http://www.openradar.me/radar?id=4943586562932736


This is a Stackoverflow thread about the issue: https://stackoverflow.com/questions/13777446/ios-how-to-get-stack-trace-of-an-unhandled-stdexception


I've tried all sorts of workarounds, including printing the stack trace with [NSThread callStackSymbols] in the constructor for the exception I use. This sort of works in debug builds (at least I can see the trace, but Crashlytics still lumps them all into "std::terminate"). But this does not work in release builds. The symbols are random symbols that don't correspond at all to the actual stack trace. The end result is that I have no usable stack information for any crashes on the main thread from C++ exceptions, which is unfortunate because my whole app is written in C++.


It's been several years since this was reported to Apple. The proper solution is to allow us to turn off the CFRunLoop exception trapper. Should I file another radar (hopefully it won't also be closed as duplicate) or what else should I do to draw Apple's attention to this? Being unable to report C++ exceptions is a pretty big deal.

Answered by DTS Engineer in 359726022

Bug report is here

Thanks! I looked at your bug (r. 50481364) and it’s definitely landed in the right place and been interpreted correctly. Alas, I can’t offer any predictions as to when it might be resolved.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Apple platforms in general don't support exceptions, even if the underlying languages do. If your code uses exceptions, or calls any Apple code that could throw exceptions, then you need to wrap your code in exception handlers. Considering how much effort that is likely to be, it is easier to just not throw exceptions. That will leave you with a much smaller set of places where you still have to catch Apple-thrown exceptions. They aren't common, but they do exist, and not always where you might expect.

Should I file another radar … ?

Yes. The bug you mentioned (r. 29407867) was closed as a dup of another bug (r. 28409982) that has since been closed. That second bug was closed as fixed, but the fix isn’t what you were looking for [1]. The new behaviour is correct from the run loop’s perspective, but if it’s causing problem with your third-party crash reporter then it’s reasonable for you to file a new bug about that.

Having said that, I strongly discourage folks from using third-party crash reporters. I have a lot to say on this subject in my Implementing Your Own Crash Reporter post.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] Specifically:

  • The previous behaviour is that it would catch the language exception and attempt to keep running the run loop. That is never a good idea.

  • The new behaviour is that it catches the language exception and terminates the process, which is a marked improvement.

  • I’m not exactly sure what you want it to do, because it depends on how your third-party crash reporter is expecting to be presented with language exceptions. I recommend that you research that so that you can file a focused bug.

Thank you for your response.


The use of a 3rd party reporter (which provides a lot more features regarding analytics that are crucial to analysis of crashes) isn't the issue. The Apple crash report for a C++ exception shows the same thing Crashlyics shows, which is always this:


8 libc++abi.dylib 0x1a27a0838 std::__terminate(void (*)()) + 16

9 libc++abi.dylib 0x1a27a0434 __cxa_rethrow + 144

10 libobjc.A.dylib 0x1a27abbc8 objc_exception_rethrow + 44

11 CoreFoundation 0x1a355c3c0 CFRunLoopRunSpecific + 544

12 GraphicsServices 0x1a575c79c GSEventRunModal + 104

13 UIKitCore 0x1cf9cfb68 UIApplicationMain + 212

14 -------------- 0x1009ded50 main (main.m:16)

15 libdyld.dylib 0x1a30228e0 start + 4


Because it catches and rethrows the exception. All information about where it came from is lost.


What I want the run loop to do is (optionally) simply not catch C++ exceptions. If all it does when catching an exception is call terminate, which is what happens when an uncaught exception is thrown, what's the point? The only effect as far as I can tell of catching and rethrowing it is that terminate always gets called from the run loop stack instead of the stack where the exception originated, which makes debugging much more difficult.


@john, wrapping the code in an try-catch won't fix this problem, in fact that *is* the problem. The CFRunLoop wraps everything on the main thread in a try-catch loop, which is why the stack frame is lost. Refactoring the code to not use C++ exceptions is not really an acceptable option either. Exceptions are, for many reasons, the preferred way to handle exceptional situations/errors in modern C++ code (differences of opinion notwithstanding, I've read both sides and made the decision judiciously), and while it would be unfortunate to be unable to analyze crashes originating from Apple platforms, I'd be more willing to do that than introduce design workarounds for a code base designed to support many platforms.


I already filed a bug report with Apple, hopefully they will respond. As I said, I would think that if this impacts all C++ exception-throwing code in iOS apps it's severe enough to warrant attention.

The Apple crash report for a C++ exception shows the same thing Crashlyics shows …

I was surprised by that assertion (I see a lot of crash reports with exception information in them), so I decided to run some tests. It turns out that, when the exception gets caught by the run loop, the last exception information only makes it into the crash report for Objective-C exceptions, not for C++ ones. That surprised me because Objective-C and C++ use the same exception mechanism on modern systems. It seems that the code to record the backtrace and render it to the Last Exception Backtrace section of the crash report is Objective-C specific.

What I want the run loop to do is (optionally) simply not catch C++ exceptions.

And, realistically, if someone were implementing the run loop today, that’s how it’d work. The reasons why the run loop works the way it currently works are mired in the depths of history.

I already filed a bug report with Apple, hopefully they will respond.

What was the bug number? I want to make sure it’s clear about two points:

  • That this affects Apple crash reports, and thus any mention of third-party crash reports is a red herring

  • That this is specific to C++ exceptions

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Bug report is here: https://bugreport.apple.com/web/?problemID=50481364
There is no mention of third party reporters in the bug description, although the sample stack trace I uploaded shows some Crashlytics functions in the stack. I just uploaded another one that is identical to what I posted in the reply above.


Thank you for looking into this! Let me know if you need any more info.

Accepted Answer

Bug report is here

Thanks! I looked at your bug (r. 50481364) and it’s definitely landed in the right place and been interpreted correctly. Alas, I can’t offer any predictions as to when it might be resolved.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Is there an update to this? The links seem not to work now that Feedback Assistant is being used and I don't see a way to look up by bug number. We have all of our c++ exception crashes being rethrown and its making it impossible to debug. Has a way been made to get around this?

You cannot look up someone else's bug report.


As I mentioned above, it simply isn't feasible to use exceptions on Apple's platforms. They just aren't supported. You would have to wrap virtually every method in an exception handler. I don't think sweatervest understood what I meant. I meant every single method. That just isn't feasible. Unfortunately, Apple's official position is that exceptions only exist to highlight errors in your code and the only appropriate response to them is crashing your app. I realize it is a catch-22. But Apple doesn't want to implement proper exception handling any more than anyone else does. Lacking any other viable option, there is no choice left except ripping out all exceptions. Then you only have to worry about the exceptions that Appwl throws, which is admittedly a much smaller problem. I suggest replacing your exceptions with a call to your own logging system and then terminate the app. Don't use Apple's loggin system. That's another can-o-worms.

DavidJH wrote:

Is there an update to this?

Not really. The bug in question remains unfixed in iOS 13 (r. 50481364).

john daniel wrote:

As I mentioned above, it simply isn't feasible to use exceptions on Apple's platforms.

I disagree with this. Attempting to recover from a thrown Objective-C exception is problematic, but it’s perfectly reasonable to throw a C++ exception and catch it within your own code. There are numerous system frameworks that do that (I encounter this all the time in the Security framework on the Mac).

However, you must ensure that the exception isn’t thrown across system stack frames, because the system won’t clean up properly in that case. Nor, as the bug above shows, will it log a helpful backtrace if the C++ exception goes completely unhandled.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

As I said, you would have to wrap virtually every single method with an exception handler. Maybe if you had one long-running background thread that called other functions, those functions could safely throw exceptions if the calling method caught them. But you have to be careful not to accidentally call any of those methods from IB action, or heaven forbid some Document or View override.


So that's why I say it isn't feasible. Sure, it's possible. But no one is ever going to do it properly, so why get their hopes up.

John, I don't know what you are talking about. My code base uses C++ exceptions and they work as intended. I don't know what you mean by Apple's implementation of exception handling being "improper". They throw and are caught exactly as the language specifies (at least with C++, I've never really used ObjC exceptions). Of course the response to an uncaught exception is to crash the app. What else would it be? Also I don't know why you think refactoring an entire code base, especially one that is multi-platform, to not use exceptions is any more feasible than adding try-catch blocks. Errors indicate an inability to proceed forward unless the code expects and can handle the error case, so making an error something that could quietly pass is setting myself up for crashes that are far harder to debug than what I have now. If I used a logger that called abort, the equivalent issue would be forgetting to call the logger on an unhandled error, and then the app keeps running until it crashes later for non-obvious reasons. I very much want the app to stop executing if an exception occurs and no try-catch was in place to indicate it has been properly handled. In fact since I use such strict type safety, it wouldn't be possible to code to this. I throw exceptions in functions that can't return a validly constructed instance of whatever type they return. Your suggestion of logging is what I already do but it's added to the log of a thrown exception (see below).


David, a partial workaround is to define your own Exception class and use it in as many places as possible as a base for all your exceptions. Have this class capture the stack trace on construction and add it to the "what". This gets around the problem for debug builds, but for me it doesn't help with release builds because apparently the symbols aren't matching up (the stack trace it produces is obviously invalid). I may have something configured incorrectly in my project but I haven't dug around enough to find out for sure.


eskimo I don't know if you have any insight on that. Apparently [NSThread callStackSymbols] isn't reliable on release builds with all the optimizations turned on? Definitely on App Store builds and also on ad-hoc release-mode builds IIRC. What's odd is that it prints out actual symbols from the code, just not the right ones. Maybe I need to capture the raw stack frames and try to desymbolicate them afterword?

I never said that exceptions don't work. I said they are not feasible on Apple platforms. This is because Apple frameworks are explicity said to be not "exception safe". Personally, I had never heard that term before. Back when Apple introduced a far more powerful and efficient exception system in Objective-C, I assumed that I could use it. And I can definitely use those exceptions, but not with any Apple API.


Anytime you throw an exception, you have to make sure that your thrown exception will not jump over an Apple stack frame. So if you are some some class derived from an Apple class, such as NSDocument or NSView, it is never safe to throw an exception unless you catch that exception right away, in the same method where you throw it. Modern Apple APIs make heavy use of blocks. If you throw an exception in a block, or from any function that has been called from from your block, you must catch that exception before jumping out of the block stack frame.


I never said refactoring a code base would be easy. But if you remove all exceptions, you can easily verify that and you have a guarantee that exceptions won't cause a problem. If you are still using exceptions, you would have to analyze each and every possible logic path to ensure that a thrown exception never leaves your code and never skips over any Apple code. I am a big fan of exceptions and I tried to throw and catch exceptions safely in my own code. It just wasn't worth the effort.


I don't know what your code is like. If it is cross platform, and completely isolated from Apple's code, then you may be able to freely use exceptions. But you started this thread talking about iOS and run loops, and doesn't sound cross-platform to me. A run loop is Apple code. If that run loop is calling a method you wrote, and that method or any called method throws exceptions, then you must catch those exceptions before returning back to the run loop. If the run loop calls Apple code, which then calls one of your methods, via override, notification, delegate, etc. then you still have to catch the exceptions. In normal C++ code, you throw lots of exceptions, but have relatively fewer places where you catch them. In order to make exceptions work on an Apple platform, it would be essentially the opposite. You will need much more places where you catch exceptions as compared to where you throw them. The way I see it, that effectively negates the benefits of using exceptions in the first place.

Maybe I need to capture the raw stack frames and try to desymbolicate them afterword?

Yes, that’s what I’d recommend. On-device symbolication isn’t really feasible because a lot of the required infrastructure simply isn’t present on the device. This is especially true for iOS and for the more advanced languages, like C++ and Swift. If you’re going down this path — and that makes sense given the bug at the centre of this issue — you should capture the backtrace addresses and then symbolicate off-device.

atos
is your friend.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Any update to this? We also are looking for a way to turn of this catch-all of C exceptions by the RunLoop, so we get proper tracebacks in crash reports from uncaught C exceptions. Also makes this debugging easier, as it will break directly at the throw site.

Any update to this?

Only to say that this problem is not resolved in the currently seeded new OS releases )-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
C++ exceptions on iOS
 
 
Q