Swift Exception Handling in Apple OSes

I have an app whose logic is in C++ and rest of the parts (UI) are in Swift and SwiftUI.

Exceptions can occur in C++ and Swift. I've got the C++ part covered by using the Linux's signal handler mechanism to trap signals which get raised due to exceptions.

But how should I capture exceptions in Swift? When I say exceptions in Swift, I mean, divide by zero, force unwrapping of an optional containing nil, out of index access in an array, etc. Basically, anything that can go wrong, I don't want my app to abruptly crash... I need a chance to finalise my stuff, alert the user, prepare diagnostic reports and terminate. I'm looking for a 'catch-all' exception handler. As an example, let's take Android. In Android, there is the setDefaultUncaughtExceptionHandler method to register for all kinds of exceptions in any thread in Kotlin. I'm looking for something similar in Swift that should work for macOS, iOS & iPadOS, tvOS and watchOS.

I first came across the NSSetUncaughtExceptionHandler method. My understanding is, this only works when I explicitly raise NSExceptions. When I tested it, observed that the exception handler didn't get invoked for either case - divide by zero or invoking raise.

class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        
        Log("AppDelegate.applicationDidFinishLaunching(_:)")
        
        // Set the 'catch-all' exception handler for Swift exceptions.
        Log("Registering exception handler using NSSetUncaughtExceptionHandler()...")
        NSSetUncaughtExceptionHandler { (exception: NSException) in
            Log("AppDelegate.NSUncaughtExceptionHandler()")
            
            Log("Exception: \(exception)")
        }
        Log("Registering exception handler using NSSetUncaughtExceptionHandler() succeeded!")
        
        // For C++, use the Linux's signal mechanism.
        ExceptionHandlingCpp.RegisterSignals()
        
        //ExceptionHandlingCpp.TestExceptionHandler()
        AppDelegate.TestExceptionHandlerSwift()
    }

   static func TestExceptionHandlerSwift() {
        Log("AppDelegate.TestExceptionHandlerSwift()")
        
        DivisionByZero(0)
    }

    private static func DivisionByZero(_ divisor: Int) {
        Log("AppDelegate.DivisionByZero()")
        
        let num1: Int = 2

        Log("Raising Exception...")
        //let result: Int = num1/divisor
        let exception: NSException = NSException(name: NSExceptionName(rawValue: "arbitrary"), reason: "arbitrary reason", userInfo: nil)
        exception.raise()
        
        Log("Returning from DivisionByZero()")
    }
}

In the above code, dividing by zero, nor raising a NSException invokes the closure passed to NSSetUncaughtExceptionHandler, evident from the following output logs

AppDelegate.applicationWillFinishLaunching(_:)
AppDelegate.applicationDidFinishLaunching(_:)
Registering exception handler using NSSetUncaughtExceptionHandler()...
Registering exception handler using NSSetUncaughtExceptionHandler() succeeded!
 ExceptionHandlingCpp::RegisterSignals()
....
AppDelegate.TestExceptionHandlerSwift()
AppDelegate.DivisionByZero()
Raising Exception...

Currently, I'm reading about ExceptionHandling framework, but this is valid only for macOS.

What is the recommended way to capture runtime issues in Swift?

Answered by DTS Engineer in 813035022

It’s better if you reply as a reply rather than in the comments; see Quinn’s Top Ten DevForums Tips for this and other titbits.

If the signal handler is invoked with a signal like SIGSEGV or SIGBUS, then there's nothing much to do but to log some diagnostic info

Yep, that’s what so treacherous about the signal handling mechanism. It looks easy, but in reality it’s super hard.

Let’s start with the question of how to “log some diagnostic info”. Can you do that using only async signal safe APIs? So, no calls to malloc, no calls to Swift or Objective-C, no calls to the dynamic linker, and no calls to any system routine that could possibly call these?

Oh, and if you try to get around this by pre-allocating stuff then you have to worry about thread A taking a signal while you’re already handling one for thread B.

Your diagnostic info will probably want to generate a backtrace, right? You can’t use system routines like backtrace because those aren’t signal safe. So you have to backtrace the thread yourself, which means you’re writing architecture-specific code. Moreover, there are edge cases that are hard to handler; Implementing Your Own Crash Reporter explains those.

Also, consider what happens if the thread crashed because it ran out of stack space. To handle that you need to enable the signal alternative stack (sigaltstack) but that’s a per-thread state and can only be enabled by the thread itself.

Worse yet, consider what happens if the thread crashed with very little stack space, just enough to create the cross-signal-handler stack frame and get your code running. If you then call another routine, your might run off the end of the stack. And you can’t avoid that with an alternative stack because sigaltstack isn’t an async signal safe function.

The upshot of this is that, if you implement your own crash reporter, it will work most of the time but in some cases it will crash. At that point you’ve lost the state of the original crash that you’re trying to debug. So you’re writing a lot of gnarly code that makes it harder to debug the trickiest of crashes.

Array's out of index, division by zero etc... there are no ways to handle these kinds of errors, which are programmer bugs, in Swift. Is that right?

Basically, yes.

Let’s focus on array out of bounds for the moment. If you access an array out of bounds, the Swift standard library detects that and traps. This trap manifests as a machine exception. There’s no way to catch that machine exception without also being exposed to machine exceptions coming from other parts of your process. Handling those in the general case is hard, per my explanation above.

The same is true for other Swift runtime: a failed force unwrap, overflow, division by zero, and so on.

Swift’s policy on such errors is that they should kill your process in order to prevent security problems and potential data corruption. This policy doesn’t sit well with all Swift users. Most notably, it causes problems for the Swift-on-server folks and for Swift Testing. If you want to engage in that debate, you can do that over on Swift Evolution.

IMPORTANT Before you post anything, search the Swift Forums for previous discussions on this topic.

Share and Enjoy

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

Signal handling is a Unix functionality that pre-dates Linux. Technically, it's more accurate to say that Linux signal handling came from macOS (via BSD), not the other way around. But this mechanism is not related to language runtime exceptions.

Apple has its own perspective about exception handling that is likely different than what you are expecting. I can't speak for Apple regarding what their specific philosophy might be.

I would suggest that you not worry too much, or at all, about signal handling. If you did need support for some specific signals, then you can add that support as appropriate.

I'm not sure what you are saying about the relationship between C++ exceptions and signal handling. I recommend that you handle all C++ exceptions before they could be exposed to any higher levels or Apple APIs such as Objective-C, Swift, or especially SwiftUI.

If there are any Swift APIs that depend on Swift exceptions, then you will need to handle those. But I wouldn't recommend building too much of your own exception-based architecture beyond what is required for Apple APIs. Your expectations about how exceptions should behave and interact with the system might not be the same as what Apple intends.

When I say exceptions in Swift, I mean, divide by zero, force unwrapping of an optional containing nil, out of index access in an array, etc.

Those are programmer errors which of course ideally (!) should never occur in shipping code. They are treated as unrecoverable by the Swift runtime. You should make sure to distinguish these from error conditions that can occur due to run time conditions rather than errors not caught at development time. Swift’s error mechanism is designed to handle the latter but not the former.

Consider: if your code tries to execute one of those serious programmer errors, then clearly the app is already off the rails in some way, so any recovery logic you want to attempt may be unhelpful at that point. The app should be considered unstable and broken.

In Android, there is the setDefaultUncaughtExceptionHandler method to register for all kinds of exceptions in any thread in Kotlin. I'm looking for something similar in Swift

There is no equivalent in Swift.

Currently, I'm reading about ExceptionHandling framework

That’s for Objective-C. The exception mechanisms of Objective-C and Swift are totally separate.

I need a chance to finalise my stuff, alert the user, prepare diagnostic reports and terminate.

I think Quinn's post about "why not to write your own diagnostic report generator" post, if you can find it, could be relevant here.

Here it is: Implementing Your Own Crash Reporter. To jump quickly to the section on signals, search for “gaping pitfalls.”

To jump quickly to the section on signals, search for “gaping pitfalls.”

What an excellent turn of phrase (-:

The other DevForums post I wanna reference is What is an exception?, which defines the terminology we use an Apple platforms. It also explains why it’s best to steer clear of Exception Handling framework.

GangOrca, Please read these through and post back here with your follow-up questions.

Share and Enjoy

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

I was able to capture these machine exceptions (I've only tried with 2 signals, not all) in C++ using Signal handler mechanism. Is that okay?

The point of it being super hard is that it usually isn't done properly. If any of these exceptions actually happen in the real work, this kind of carefully crafted exception handling logic will usually fail.

Years ago, the approach you describe was popular. I remember large government projects where teams worked for weeks and all they accomplished was a huge mess of complex exception handling code. They held a code review for it but they hadn't actually implemented any of the actual logic for the mission. It was literally just exception handling. Don't do that. At least they were being paid for it. Are you?

Regarding Swift and Objective-C exceptions, there are some subtle, but very important differences. Objective-C has both exception handling mechanisms and error handling mechanisms. They are not the same. Exceptions are supposed to crash the app. Errors should be handled and/or reported to the user.

Swift's exception handling is based on Objective-C's error handling. You are expected to avoid even the possibility of things like out of bounds, division by zero, etc. But if function throws an exception, you are expected to handle it. In fact, Swift will require you to handle it. Swift 6 concurrency adds more exception handling requirements.

But none of these Swift exceptions are the exceptions that you've been worrying about. You've done all this work, but you haven't actually addressed any of the exceptions that you will need to handle.

It’s better if you reply as a reply rather than in the comments; see Quinn’s Top Ten DevForums Tips for this and other titbits.

If the signal handler is invoked with a signal like SIGSEGV or SIGBUS, then there's nothing much to do but to log some diagnostic info

Yep, that’s what so treacherous about the signal handling mechanism. It looks easy, but in reality it’s super hard.

Let’s start with the question of how to “log some diagnostic info”. Can you do that using only async signal safe APIs? So, no calls to malloc, no calls to Swift or Objective-C, no calls to the dynamic linker, and no calls to any system routine that could possibly call these?

Oh, and if you try to get around this by pre-allocating stuff then you have to worry about thread A taking a signal while you’re already handling one for thread B.

Your diagnostic info will probably want to generate a backtrace, right? You can’t use system routines like backtrace because those aren’t signal safe. So you have to backtrace the thread yourself, which means you’re writing architecture-specific code. Moreover, there are edge cases that are hard to handler; Implementing Your Own Crash Reporter explains those.

Also, consider what happens if the thread crashed because it ran out of stack space. To handle that you need to enable the signal alternative stack (sigaltstack) but that’s a per-thread state and can only be enabled by the thread itself.

Worse yet, consider what happens if the thread crashed with very little stack space, just enough to create the cross-signal-handler stack frame and get your code running. If you then call another routine, your might run off the end of the stack. And you can’t avoid that with an alternative stack because sigaltstack isn’t an async signal safe function.

The upshot of this is that, if you implement your own crash reporter, it will work most of the time but in some cases it will crash. At that point you’ve lost the state of the original crash that you’re trying to debug. So you’re writing a lot of gnarly code that makes it harder to debug the trickiest of crashes.

Array's out of index, division by zero etc... there are no ways to handle these kinds of errors, which are programmer bugs, in Swift. Is that right?

Basically, yes.

Let’s focus on array out of bounds for the moment. If you access an array out of bounds, the Swift standard library detects that and traps. This trap manifests as a machine exception. There’s no way to catch that machine exception without also being exposed to machine exceptions coming from other parts of your process. Handling those in the general case is hard, per my explanation above.

The same is true for other Swift runtime: a failed force unwrap, overflow, division by zero, and so on.

Swift’s policy on such errors is that they should kill your process in order to prevent security problems and potential data corruption. This policy doesn’t sit well with all Swift users. Most notably, it causes problems for the Swift-on-server folks and for Swift Testing. If you want to engage in that debate, you can do that over on Swift Evolution.

IMPORTANT Before you post anything, search the Swift Forums for previous discussions on this topic.

Share and Enjoy

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

Swift Exception Handling in Apple OSes
 
 
Q