How do you read a crash report, what do the exception types and Swift runtime messages mean, and how does symbolication with dSYMs work?

23.5.1 · L3 · Interview

Predict first

Three crashes: force-unwrapping nil, an out-of-range index, and try! on a throwing call. What do the exit status and message look like?

// === main.swift ===
import Foundation

struct Oops: Error {}
func fail() throws { throw Oops() }

func explode(_ kind: Int) {
    let items = [1, 2, 3]
    let maybe: Int? = kind == 99 ? 1 : nil
    switch kind {
    case 1: print(maybe!)
    case 2: print(items[kind + 5])
    case 3: try! fail()
    default: break
    }
}
explode(Int(CommandLine.arguments[1])!)
  • Each exits with status 133 and prints a different “Fatal error” message
  • Each exits with status 0 after logging
  • They can be caught with do/catch

Predict first

A crash address is symbolicated with atos, once with the dSYM and once with only the stripped binary. What do you get, and do the UUIDs match?

// === main.swift ===
import Foundation

struct Oops: Error {}
func fail() throws { throw Oops() }

func explode(_ kind: Int) {
    let items = [1, 2, 3]
    let maybe: Int? = kind == 99 ? 1 : nil
    switch kind {
    case 1: print(maybe!)
    case 2: print(items[kind + 5])
    case 3: try! fail()
    default: break
    }
}
explode(Int(CommandLine.arguments[1])!)
  • With the dSYM: function and line; without: just an offset; UUIDs match
  • Both print the function and line
  • Neither can be symbolicated

Mental model. A crash report is a flight recorder. The exception type says how it ended, the trap message says why, and the dSYM is the decoder ring that turns coordinates into street names.

A crash report (an .ips file) starts with who and how: the app, version, OS and device, then the exception type and codes, the termination reason, the triggered thread, and a backtrace per thread, followed by the list of binary images with their load addresses and UUIDs. Read it from the top. EXC_BAD_ACCESS (SIGSEGV or SIGBUS) is an invalid memory access (a dangling pointer, a nil deref in unsafe code, stack overflow from runaway recursion). EXC_BREAKPOINT (SIGTRAP) is how a Swift runtime trap shows up: a force-unwrap of nil, an out-of-range index, try! on a thrown error, integer overflow, precondition and fatalError, all of which exit with a trap (status 133, shell code 128 + 5 in the check below), and the crash’s message text (“Unexpectedly found nil while unwrapping an Optional value”, “Index out of range”, “'try!' expression unexpectedly raised an error”) tells you which. EXC_CRASH with SIGABRT is an explicit abort, typically an uncaught Objective-C exception, whose reason names the exception. A termination such as 0x8badf00d is the watchdog (Q228), and a jetsam kill (Q227) produces no crash report at all. Swift errors and traps cannot be caught with do/catch; only thrown errors can.

A raw report has addresses; symbolication maps them to function names, files and lines using the build’s debug symbols. Release builds keep symbols in a separate dSYM bundle, matched to the binary by UUID (the check below shows the same UUID in the binary, its dSYM and the stripped copy). Xcode symbolicates crashes from the Organizer if it can find the dSYM (archives keep it); third-party crash reporters need you to upload the dSYM for every build, which is why a CI step should do it; atos symbolicates one address by hand (with the dSYM it prints the function and line, without it only an offset), and symbolicatecrash handles a whole report. Without the right dSYM you only get addresses, so keep every shipped build’s dSYMs (and rebuilds are not guaranteed to produce the same UUID). Always match app version and build to the report.

Swift runtime traps stop the process with a trap signal and a message that identifies the cause.

Stripping removes the names from the binary. The dSYM, matched by UUID, keeps them, which is why you must archive it.

Common traps

  • Losing the dSYM of a shipped build. The crash can then never be symbolicated; archive and upload them in CI.
  • Trying to catch a Swift trap with do/catch. Force-unwrap, index and overflow traps end the process; fix the cause or validate input.

They might follow up with

  • What does EXC_BREAKPOINT usually mean in a Swift app? A runtime trap: unwrap, bounds, try!, overflow, precondition or fatalError.
  • How does a symbolicator find the right symbols? By the binary’s UUID, which must match the dSYM’s.

Related questions

  • 23.5.2 How do you build a production diagnostics pipeline: collection, symbolication, grouping, prioritisation…
  • 23.3.2 Why do some bugs appear only in Release builds, how do assert, precondition and fatalError differ, and how do…
  • 6.6.2 What is jetsam, and how does it differ from a crash?
  • 6.6.3 What is the watchdog, and what causes the 0x8badf00d exception?
  • 21.3.1 What does MetricKit give you in production, how do you subscribe, and what is the difference between metric…

How do you build a production diagnostics pipeline: collection… →