How to print call stack in Swift?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Swift provides Thread.callStackSymbols to get the current call stack as an array of strings. Each string represents a stack frame with the binary name, address, function name, and offset. This is useful for debugging, logging crash contexts, and understanding execution flow. For production crash reporting, use structured frameworks like os_log or third-party tools like Sentry or Firebase Crashlytics instead.
Thread.callStackSymbols
Output (debug build):
Each line contains: frame index, binary name, address, demangled symbol name, and byte offset.
Thread.callStackReturnAddresses
For programmatic access, use the raw return addresses:
Return addresses are NSNumber values. You can symbolicate them later with atos or dSYM files.
Using in a Custom Logger
Assertions and Preconditions with Stack Traces
Swift's assertionFailure and preconditionFailure automatically print the call stack. You can also create custom assertion helpers:
Exception Handler for Uncaught Exceptions
Capture the stack trace when an uncaught Objective-C exception occurs:
This handler catches Objective-C exceptions (NSException) but not Swift runtime errors (fatalError, preconditionFailure).
Signal Handlers for Swift Crashes
For pure Swift crashes (EXC_BAD_ACCESS, SIGABRT):
Signal handlers are limited — you cannot allocate memory or call most functions safely. For production, use a crash reporting framework.
Symbolication
In release builds, symbols are stripped. Stack traces show addresses instead of function names:
To symbolicate, use atos:
os_log and OSSignposter (Modern Approach)
For structured debugging in production:
os_log messages are captured by Console.app and can be filtered by subsystem and category.
Common Pitfalls
- Mangled symbols in release builds: Without dSYM files, release build stack traces show hexadecimal addresses. Always archive dSYM files for every release build. Xcode Archives include them automatically.
- Performance overhead of
callStackSymbols: Generating human-readable symbols is expensive (involves dladdr lookups). Do not callThread.callStackSymbolsin hot code paths. UsecallStackReturnAddressesif you only need raw addresses for later symbolication. - Signal handler limitations: Signal handlers run in a restricted context. Calling
print(), allocating memory, or using Objective-C runtime functions in a signal handler is undefined behavior. Usewrite()(POSIX) for minimal crash logging. - Missing frames due to tail call optimization: The compiler may optimize tail calls, removing frames from the stack. In debug builds (
-Onone), all frames appear. In release builds (-O), some intermediate frames may be missing. - NSSetUncaughtExceptionHandler does not catch Swift errors: Swift's
fatalError(), array out-of-bounds, and force-unwrap failures are not Objective-C exceptions. The uncaught exception handler only catchesNSException. Use signal handlers for Swift runtime crashes.
Summary
- Use
Thread.callStackSymbolsto get the current call stack as an array of readable strings - Use
Thread.callStackReturnAddressesfor raw addresses (faster, for later symbolication) - In release builds, use dSYM files and
atosto translate addresses to function names and line numbers - Use
NSSetUncaughtExceptionHandlerfor Objective-C exceptions and signal handlers for Swift crashes - Avoid calling
callStackSymbolsin performance-critical code — it is expensive - For production apps, use structured logging (
os_log) and crash reporting frameworks (Sentry, Crashlytics)

