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?
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
orSIGBUS
, 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"