How to Get Stack Trace Error in Swift

How Do I Recover a Stack Trace in Swift?

In many cases, you could use a try/catch mechanism.

do {
try ... // risky business goes here
} catch let error as NSError {
print("Error: \(error.domain)")
println(NSThread.callStackSymbols())
}

Swift errors have no such thing as stack-trace yet (if will ever), and even Xcode can show stack-trace only if the error is un-handled by our code (and gets caught by Xcode directly instead).

Alternativly, your custom Error's constructor can store the stack-trace for later use, but in most cases errors are not custom, where you can't alter error's constructor (like errors of 3rd-party library).

Exception breakpoint

If you just want to debug, without need to upload stack-trace to server, then Xcode's "Exception breakpoint" feature can help, like:

  • First place a normal-breakpoint near the failing logic.

  • Wait until Xcode pauses App on that line, enable Xcode's feature:

    Sample Image

    Sample Image

  • Finally, resume App, and wait untill exception is thrown.

Images are old, nowadays you see "Swift error breakpoint" or something like that as well (beside "Add Exception Breakpoint" option).

How to pass an Error up the stack trace in Swift

Referring to Swift - Error Handling Documentation, you should:

1- Create your custom error type, by declaring enum which conforms to Error Protocol:

enum CustomError: Error {
case error01
}

2- Declaring foo() as throwable function:

func foo() throws {
throw CustomError.error01
}

3- Declaring bar() as throwable function:

func bar() throws {
try foo()
}

Note that although bar() is throwable (throws), it does not contain throw, why? because it calls foo() (which is also a function that throws an error) with a try means that the throwing will -implicitly- goes to foo().

To make it more clear:

4- Implement test() function (Do-Catch):

func test() {
do {
try bar()
} catch {
print("\(error) has been caught!")
}
}

5- Calling test() function:

test() // error01 has been caught!

As you can see, bar() automatically throws error, which is referring to foo() function error throwing.

Getting a human readable stacktrace from swift

So I think first of all that re-implementing a crash reporting solution like Firebase Crashlytics is non-trivial and NOT advisable given the quality of the service that Crashlytics provides for free. Crash reporting is hard because you need to avoid crashes in the framework itself because of low-level interfacing with the memory addresses, function pointers, OS signals, and multithreading without type safety and garbage collection (in C++). It is also notoriously hard to test giving the wide variety of possible crashes, app states, different devices (needs to be lightweight for older devices), and edge cases with deadlocks/threading/memory leaks. If you STILL want to go ahead, I'll outline the main considerations below.

The issue with runtime symbolication is implementing the API to read the debug symbols, upload symbols, and intercept exception handlers. callStackSymbols is not intended to be machine-readable so you'd have to reimplement the call stack parser using functions like callStackReturnAddresses.

This is non-trivial because not only does the crash have to be symbolicated, but needs to be uploaded to the server/cached (usually done by busy waiting while checking if the app is still active) before the app is terminated because of the crash. This means that the code needs to run highly efficiently to generate the stack frame with file and line number metadata (C++ is best for that).

It's challenging to perform asynchronous uploads without high-level C++ libraries, and do persistent local caching without using the native Foundation APIs (the events also need to be queued on a dedicated thread, using locks to ensure synchronisation). In addition to language barriers, the names need to be symbolicated and demangled with thread information (each stack frame needs to also include the state of every other thread at the time). The crash stack trace does include the method name by default (mangled), but additional information like the file name and line number needs to be uploaded at app runtime. The hex numbers you are seeing are memory addresses, so these need to be converted at runtime to the appropriate object metadata and sent. The way to intercept exceptions in C is to use Signal Handlers - intercepting the iOS kernel signals for:

{SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSYS, SIGTRAP};

If the signal is not one of these, you can still send a request but use an "unknown" signal handler to intercept the crash.

That being said it's probably feasible to stitch together a crash reporter using debug symbols. You can do that just once when you've archived the app (you don't need a physical device). You can do this once when sending the app to the App Store. Fastlane can make this easier with an output directory (an alternative is to use the environment variable DWARF Destination Folder with xcodebuild). You can then parse debug symbols on the server.

Find the .xcarchive and do this to zip together the dsyms of the main target and any linked frameworks:

ditto -c -k --keepParent -rsrc <AppName>.xcarchive/dSYMs/*.dSYM <AppName>-${PRODUCT_VERSION}-dSYM.zip

You mentioned you don't know where to find the debug symbols. If you want to know where the archives are located, you can go to the Xcode Organizer, Archives, and then right-click Show in Finder. Then right click to Show Package Contents to see the dSYMs.

Even if you are happy with a fairly rudimentary crash reporter you also need to encrypt server uploads as the codebase could be reverse-engineered when sending the raw crash logs! You also would need to manually check the Build UUIDs to ensure the same app version which crashed is being symbolicated (the symbolication will fail otherwise).

An alternative to this would be to use crash logs provided by the App Store, (downloadable in Xcode). The advantage of this is mainly ease of setup, integration with Xcode, and quality of reports. You can also get Jetsam memory log warnings if you stick with Apple. The server itself would then be easily able to automate crash report construction using the dSYMs and a script:

//Put dsyms and crash log in one folder and navigate to that folder
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
cp -i /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash ./
./symbolicatecrash unsymbolicated.crash > symbolicated.crash

I hope this pointed you in the right direction; it's unfortunately not possible to give you the complete code for a fully functional crash reporting solution (it's too broad, complex, and not to mention WAY out of scope for a SO question). :-)

How to find the exact exception that's being occured or the root cause from the crash logs ios

Unrelated, but in Swift, there is no need for ; at the end of the lines.

Let's read the crash log:

2   watch Extension                 0x04488074 closure #1 in OtpView.fetchProfile() + 1232 (OtpView.swift:203)
3 watch Extension 0x0443c47c specialized closure #1 in closure #1 in useInteceptor(urlString:method:requestBody:completionHandler:) + 1120 (Inteceptor.swift:0)
4 watch Extension 0x0443dc80 partial apply for specialized closure #1 in closure #1 in useInteceptor(urlString:method:requestBody:completionHandler:) + 72 (<compiler-generated>:0)

That's the interesting part (the top ones with names of methods), you read them from the bottom.

So in Interceptor.swift, method useInteceptor(urlString:method:requestBody:completionHandler:) is called at some point.

In this one, there is a closure (or multiple ones), but at least, in the first one (closure #1), which is completionHandler, that you call in fetchProfile of OptView class in OptView.swift file.

And there, in line 203, it's the line causing the crash.

Let's analyze the culprit:

let profile = try! JSONDecoder().decode(ProfileResponse.self, from: data)

Here, the possible crash is because of try!.

Why did you use try! instead of writing a do/try/catch? Do you know why you used a force unwrap ! here? What it means?

It means that if an error is thrown, just make the app crash.

So if there is a crash, it's expected behavior, since you explicitly wrote "crash here if there is thrown error".

So, now, why would decode(_:from:) crash?

I don't know what's ProfileResponse, but that method could throw an error because you didn't specify to it that a value in the JSON can be nul, a value in the JSON can be omitted, because there is another issue with the received JSON, or JSON is invalid.

Or, because your API is giving a bad value. It's sometimes the cases when API encounters an error, they could responds: {"error": "some reason why it failed"}. It's a valid JSON, but I don't think that ProfileResponse expect to be like that.

Now, as why it giving bad response, it's up to your Web API, check the doc, check the API developers for possible responses: Did you use bad parameters, are you falling into the one case not handled by back-end?

So when you wrote that line with the try!, you decided to tell: "Don't worry about the response, if there is a response, I'm sure of it that it can be decoded into a ProfileResponse object. Trust me, I know what I'm doing, I guarantee it will be always valid, and if that's not the case, just crash, that would prove me wrong, but rest assured, I'm sure of myself". Yes, that what meant try!.

If you don't want to crash, don't use !, and write a property do/try/catch.

do {
let profile = try JSONDecoder().decode(ProfileResponse.self, from: data)
if let ratePlanDetails = profile.response?.detail {
self.navigateToNext.toggle()
}
} catch {
print("Oops, there was an error while decoding ProfileResponse: \(error)")
print("And the API response was: \(String(data: data, encoding: .utf8) ?? "unknown data")")
}

Now, as to why you have an invalid response, that's up to your debugging: trying to reproduce it, with specific params, specific case, etc.). We can't guess what's wrong, we can't guess what's returning the API.
Once you know what's the real response sent back by your API, and don't know how to handle it, you can ask a new question on SO and we might help you, but at this point, we can't do anything more about your issue.

Sentry: Event is missing number lines in stack trace

Please make sure that you upload your dSYMs (debug symbols) for every release build so Sentry can unscramble Apple’s crash logs to reveal the function, file names, and line numbers of the crash.

The attachStacktrace is enabled per default. This flag controls if you want to attach the stacktrace for every event you capture. For exceptions and crashes, the SDK always attaches the stacktrace. There is no need for you to enable this manually.

I recommend using the following start method. It gives you code completion.

SentrySDK.start { options in
options.dsn = "YOUR_DSN"
options.debug = true
}


Related Topics



Leave a reply



Submit