An iOS crash log looks hostile the first time you open one: forty threads, hexadecimal addresses, and a wall of system frames — with your bug buried somewhere in the middle. But the format is consistent, and once you know which five sections matter, most crash reports read like any other stack trace. This guide shows you where to find crash logs on a device, in Xcode, and in App Store Connect; how to read the report section by section; what the common exception types actually mean; and how to symbolicate raw addresses back into your own function names and line numbers.
Where to find iOS crash logs
Crash reports (.ips files since iOS 15) are written on-device the moment an app dies, and they surface in several places:
- On the device itself — Settings → Privacy & Security → Analytics & Improvements → Analytics Data. Every entry named
YourApp-2026-07-12-183501.ipsis a crash (or hang) report. Users can share them from here via AirDrop or Mail, which makes this the fallback channel when a tester says "it just closed." - Xcode → Window → Devices and Simulators — select a connected device, click View Device Logs. Xcode pulls the logs off the device and symbolicates what it can.
- Xcode Organizer (Window → Organizer → Crashes) — crash reports from App Store and TestFlight users who opted in to sharing analytics, aggregated per version and already symbolicated if you uploaded symbols.
- App Store Connect / TestFlight — TestFlight builds collect crashes from all testers; the Feedback section attaches the crash log to the tester's session.
- macOS Console.app — with a device plugged in, live-streams device logs, useful for crashes that happen before the app finishes launching.
Organizer and TestFlight only show crashes from users who opted in to sharing analytics with developers — typically a fraction of your install base. That sampling gap is the main reason production apps add their own crash reporting SDK.
The anatomy of a crash report
Strip away the noise and a crash report has five sections that matter:
Incident Identifier: 8A2F41B3-...
Hardware Model: iPhone15,2
Process: MyApp [1337]
Version: 2.4.1 (241)
OS Version: iPhone OS 17.5.1 (21F90)
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000010
Triggered by Thread: 0
Thread 0 Crashed:
0 MyApp 0x0000000102a4c9e8 0x102a40000 + 51688
1 MyApp 0x0000000102a4b120 0x102a40000 + 45344
2 UIKitCore 0x00000001a8f3c65c -[UIViewController loadViewIfRequired] + 936
...
Binary Images:
0x102a40000 - 0x102a8ffff MyApp arm64 <a1b2c3d4...> /var/containers/.../MyApp
- Header — app version and build number (which release crashed?), hardware model, and OS version. If a crash only appears on iOS 17.5, that's a lead in itself.
- Exception Type / Codes — why the process was killed (more below).
- Triggered by Thread — which thread's backtrace to read first.
- The crashed thread's backtrace — frames from the crash site (frame 0) downward. Unsymbolicated frames from your own binary show as
0x102a40000 + 51688; system frameworks usually show real symbol names because the OS symbols are known. - Binary Images — every loaded binary with its load address and UUID. The UUID is what ties the report to the matching dSYM during symbolication.
Exception types you'll actually see
| Exception | Signal | What it usually means |
|---|---|---|
EXC_BAD_ACCESS | SIGSEGV/SIGBUS | Read/write of a bad pointer: use-after-free, unowned reference outliving its object, threading race |
EXC_CRASH | SIGABRT | The runtime aborted deliberately — uncaught NSException (e.g. array index out of bounds), failed assertion |
EXC_BREAKPOINT | SIGTRAP | Swift runtime traps: force-unwrapped nil, failed forced cast, integer overflow, fatalError() |
EXC_RESOURCE | — | The app blew a resource budget (CPU, wakeups) |
Termination code 0x8badf00d | — | The watchdog killed the app — main thread blocked too long at launch, resume, or background transition |
Jetsam / EXC_RESOURCE (MEMORY) | — | Killed for memory pressure — technically not a crash, and there's no exception backtrace to read |
Two practical notes: SIGABRT with an NSException usually prints the exception message in the Last Exception Backtrace section, which is more useful than the thread backtrace. And EXC_BREAKPOINT in Swift is nearly always a force-unwrap or forced cast — check frame 0 of the crashed thread for the exact line once symbolicated. Swift's crash model differs fundamentally from exception-based languages; if your mental model is try/catch, read up on why Swift traps instead of throwing.
Symbolicating: turning addresses into code
An unsymbolicated frame like MyApp 0x102a4c9e8 0x102a40000 + 51688 is an offset into your binary, not a function name. Symbolication resolves it using the dSYM bundle produced at build time.
Xcode symbolicates automatically when it has the matching dSYM (it spotlights your Mac for it). To do a single frame by hand:
# find the dSYM for the build (UUID must match the Binary Images line)
dwarfdump --uuid MyApp.app.dSYM
# resolve: atos -o <dSYM binary> -l <load address> <frame address>
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
-l 0x102a40000 0x102a4c9e8
# → CheckoutViewController.applyCoupon(_:) (in MyApp) (CheckoutViewController.swift:142)
The rule that trips everyone up: the dSYM's UUID must exactly match the UUID in the Binary Images section. A dSYM from "the same version, rebuilt" will not work — bitcode recompiles, CI rebuilds, and archive/export mismatches all produce different UUIDs. If your frames stay hex even though you "have the dSYM," work through fixing dSYM UUID mismatches.
Reading the backtrace once it's symbolicated
A symbolicated crashed thread reads exactly like any other stack trace: frame 0 is where the process died, and you scan downward for the first frame in your own code — that's almost always where the bug lives, with the system frames above it just being the machinery that detected the problem. The general technique (and the traps, like assuming frame 0 is the culprit) is the same as for any language — see how to read a stack trace.
Don't stop at the crashed thread. For EXC_BAD_ACCESS crashes caused by races, the interesting question is what the other threads were doing to the same object — scan the remaining backtraces for your classes before concluding the crash "makes no sense."
From manual crash logs to automatic crash reporting
Manually collecting .ips files works for a handful of testers. In production it breaks down: opt-in sampling means you see a fraction of crashes, logs arrive without context, and nobody AirDrops you a crash report at 2 a.m. This is what crash reporting automates — the SDK captures every crash, uploads it with device state and breadcrumbs, symbolicates it server-side with your uploaded dSYMs, and groups duplicates into one issue with a user count.
LightTrace is Sentry-SDK-compatible, so the standard sentry-cocoa SDK works by changing one line:
import Sentry
SentrySDK.start { options in
options.dsn = "https://<key>@light-trace.robomiri.com/1"
options.releaseName = "com.example.myapp@2.4.1+241"
}
Upload your dSYMs once per build and every crash arrives symbolicated, grouped, and tied to a release — so you can watch your crash-free rate instead of decoding hex offsets one report at a time. For the full setup, see the iOS error tracking guide.
Start tracking errors in minutes
Stop reading crash logs by hand. Point the Sentry iOS SDK at LightTrace and get every crash symbolicated, grouped, and tied to the release that caused it.