Swift
Call Stack
Debugging
Programming
Stack Trace

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

swift
1func doSomething() {
2    printCallStack()
3}
4
5func printCallStack() {
6    let symbols = Thread.callStackSymbols
7    for symbol in symbols {
8        print(symbol)
9    }
10}
11
12doSomething()

Output (debug build):

 
10   MyApp        0x00000001000045a0 printCallStack() + 32
21   MyApp        0x0000000100004580 doSomething() + 16
32   MyApp        0x0000000100004560 main + 12
43   libdyld.dylib 0x00007fff6b2c3cc9 start + 1

Each line contains: frame index, binary name, address, demangled symbol name, and byte offset.

Thread.callStackReturnAddresses

For programmatic access, use the raw return addresses:

swift
1let addresses = Thread.callStackReturnAddresses
2print("Stack depth: \(addresses.count)")
3
4for (index, address) in addresses.enumerated() {
5    print("Frame \(index): \(address)")
6}

Return addresses are NSNumber values. You can symbolicate them later with atos or dSYM files.

Using in a Custom Logger

swift
1struct Logger {
2    static func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
3        let filename = (file as NSString).lastPathComponent
4        print("ERROR [\(filename):\(line)] \(function): \(message)")
5
6        // Print call stack for errors
7        print("Call stack:")
8        for (i, symbol) in Thread.callStackSymbols.enumerated() {
9            print("  \(i): \(symbol)")
10        }
11    }
12}
13
14// Usage
15Logger.error("Database connection failed")

Assertions and Preconditions with Stack Traces

Swift's assertionFailure and preconditionFailure automatically print the call stack. You can also create custom assertion helpers:

swift
1func require(_ condition: Bool, _ message: String = "Requirement failed") {
2    guard condition else {
3        print("ASSERTION FAILED: \(message)")
4        print("Call stack:")
5        Thread.callStackSymbols.forEach { print("  \($0)") }
6        assertionFailure(message)
7        return
8    }
9}
10
11require(user != nil, "User must be logged in")

Exception Handler for Uncaught Exceptions

Capture the stack trace when an uncaught Objective-C exception occurs:

swift
1NSSetUncaughtExceptionHandler { exception in
2    print("UNCAUGHT EXCEPTION: \(exception.name)")
3    print("Reason: \(exception.reason ?? "unknown")")
4    print("Call stack:")
5    for symbol in exception.callStackSymbols {
6        print("  \(symbol)")
7    }
8    // Write to a crash log file
9    let log = exception.callStackSymbols.joined(separator: "\n")
10    try? log.write(toFile: "/tmp/crash.log", atomically: true, encoding: .utf8)
11}

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):

swift
1import Darwin
2
3func setupSignalHandlers() {
4    signal(SIGABRT) { _ in
5        print("SIGABRT received")
6        Thread.callStackSymbols.forEach { print($0) }
7        exit(1)
8    }
9    signal(SIGSEGV) { _ in
10        print("SIGSEGV received")
11        Thread.callStackSymbols.forEach { print($0) }
12        exit(1)
13    }
14}

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:

 
0   MyApp   0x00000001000045a0 0x100000000 + 17824

To symbolicate, use atos:

bash
1# Symbolicate a single address
2atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x00000001000045a0
3# Output: printCallStack() (in MyApp) (ViewController.swift:42)
4
5# Or use Xcode's symbolicatecrash tool
6export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
7/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash crash.log MyApp.app.dSYM

os_log and OSSignposter (Modern Approach)

For structured debugging in production:

swift
1import os
2
3let logger = Logger(subsystem: "com.myapp", category: "network")
4
5func fetchData() {
6    logger.info("Starting fetch")
7    // ...
8    logger.error("Fetch failed - stack: \(Thread.callStackSymbols.joined(separator: "\n"), privacy: .public)")
9}

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 call Thread.callStackSymbols in hot code paths. Use callStackReturnAddresses if 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. Use write() (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 catches NSException. Use signal handlers for Swift runtime crashes.

Summary

  • Use Thread.callStackSymbols to get the current call stack as an array of readable strings
  • Use Thread.callStackReturnAddresses for raw addresses (faster, for later symbolication)
  • In release builds, use dSYM files and atos to translate addresses to function names and line numbers
  • Use NSSetUncaughtExceptionHandler for Objective-C exceptions and signal handlers for Swift crashes
  • Avoid calling callStackSymbols in performance-critical code — it is expensive
  • For production apps, use structured logging (os_log) and crash reporting frameworks (Sentry, Crashlytics)

Course illustration
Course illustration

All Rights Reserved.