Skip to content

Reproduce an iOS crash with logs and video

A crash you can’t repeat is a crash you can’t fix. The report says where the app died, but not what the person did to get there. This guide takes a crash from a report to a failing check that reproduces it on demand, with the stack, the app’s log and a video, on a simulator.

Read the report first

Get the crash report from where it happened:

  • A simulator on your Mac: ~/Library/Logs/DiagnosticReports, as an .ips file.
  • An iPhone cabled to your Mac: Xcode › Window › Devices and Simulators, then the phone’s device logs.
  • A TestFlight tester or the App Store: Xcode › Window › Organizer › Crashes, and App Store Connect.
  • A phone you can’t reach: on the phone, Settings › Privacy & Security › Analytics & Improvements › Analytics Data, the log named after your app, then Share.

Three lines tell you most of it:

  • Exception type and signal, such as EXC_BAD_ACCESS (SIGSEGV) for a bad pointer, EXC_BREAKPOINT (SIGTRAP) for a Swift runtime trap like a force-unwrapped nil or an array index out of range, EXC_CRASH (SIGABRT) for an uncaught exception.
  • The crashed thread’s frames. The first frame in your own module is where to look.
  • 0x8badf00d in the exception codes means the watchdog killed the app for blocking the main thread too long at launch or resume. That’s a hang, not a bad pointer.

If the frames are hex addresses, the report isn’t symbolicated. Xcode does it when it finds the dSYM of that exact build. Keep every shipped build’s dSYM, or the report stays unreadable.

Find the steps

Work backwards from three facts: what the app was showing, what the tester did, and what the log says. Ask for the app version and build, the iOS version, the device model and the last thing they tapped. The log of the seconds before the crash is the best clue you’ll get, so capture it on your own run:

# Simulator: stream the app's logxcrun simctl spawn booted log stream --level info --predicate 'process == "YourApp"'# Simulator: record the screenxcrun simctl io booted recordVideo crash.mov

On an iPhone, open Console.app, pick the phone in the sidebar and filter by your app’s name. Remember that print output doesn’t reach the unified log: use Logger or os_log for anything you want to read after a crash.

Turn the steps into a check

Write the flow in plain English, with the screen you expect to see at the end. Mobster fails a check as app_crashed when the app dies, even if every expectation held, so the expectation can be as small as “the screen is still there”:

# .mobster/checks/checkout-crash.yamlversion: 1name: Applying a promo code twice doesn't crash checkoutapp:  bundle: com.example.yourapp  path: .mobster/build/Build/Products/Debug-iphonesimulator/YourApp.applaunch:  args: ["-SkipOnboarding", "YES"]  url: yourapp://cartsteps:  - Tap Apply promo code, type SAVE10 and tap Apply.  - Tap Apply promo code again, type SAVE10 and tap Apply.expect:  - visible: { id: checkout_total }

Run it with the screen recording on:

mobster test .mobster/checks/checkout-crash.yaml --video

A check with steps runs Smart on your Claude or OpenAI key until it passes once, then replays the recorded steps with no model call. A launch-only check, with no steps, calls no model. A check that crashes never passes, so Smart runs its steps each time.

When the app crashes, the summary names the exception and the first frame in your code. Example output, from a test app that crashes on purpose:

✗ failed  Crash fixture stays up after launch  on iPhone 17 Pro, iOS 26.4    Crash Fixture crashed: EXC_BREAKPOINT (SIGTRAP), in specialized CrashTrigger.crashNow() (CrashFixtureApp.swift:11).    The report's Logs and Crash pane shows the stack and the app's log around the crash.

What Mobster keeps

For every run on a simulator, Mobster keeps the app’s log and, on a crash, its crash report. With --video it records the screen too. The suite folder .mobster/test-results/<suite-id>/ holds it all:

FileWhat it holds
index.htmlEach failed check, with its Logs and Crash pane: the top frames with your own in full ink, then the app’s log 10 seconds either side of the crash
runs/<run-id>/logs/app.logThe app’s unified log, from before launch to the verdict, stamped in seconds since the run started
runs/<run-id>/crashes/*.ipsThe crash report
videos/The screen as an H.264 MP4, 590 pixels wide
junit.xml<failure type="app_crashed"> with the frames, and the log window in <system-out>

How it reads each source: the log comes from xcrun simctl spawn <udid> log stream, the crash report from ~/Library/Logs/DiagnosticReports, and the video from xcrun simctl io recordVideo. Mobster names your app’s frames with the dSYM beside the .app or in .mobster/build/Build/Products, using atos. A Debug build already carries its names.

Two limits. First, macOS writes a simulator’s crash report after the crash: it took 61 seconds to 4 minutes on a Mac running many simulators and builds. mobster test waits up to 60 seconds, once, at the end of the suite, and the check still says it crashed and on which signal when the report comes later. Second, print isn’t in the log. Use Logger.

The log and the crash report can hold secrets, so each line goes through the Mobster secret filter before it’s written: passwords, one-time codes, card numbers and API keys become ••••. The files stay on your Mac.

What it costs, measured on an M5 Max running many other simulators and builds, with a launch-only check on an iPhone 17 Pro simulator, 6 runs each, interleaved: a median 8.6 s without capture and 7.6 s with it, a difference inside the noise. A simulator’s video was 2 to 7 MB a check.

Crashes your testers hit

mobster asc feedback lists the crashes from TestFlight and downloads each crash log, so you can start from the real report:

mobster asc feedback --app com.example.yourapp --crashes --save feedback/

Then write the check from the report’s device, iOS version and last screen, and run it on a simulator, where the log, the crash report and the video are captured. When the crash only happens on hardware, run the same check on a phone to reproduce the steps:

mobster test .mobster/checks/checkout-crash.yaml --device "Test iPhone" --testflight com.example.yourapp

On an iPhone a check drives only your own build and never clears its data. Keep the phone unlocked.

When it doesn’t reproduce

  • Match the conditions. A crash that needs a slow network, a full disk, a denied permission or low memory won’t show on a fresh simulator. A check can set permissions, locale, content_size and location on a simulator (conditions), and only appearance and locale on a real iPhone.
  • Run it many times. mobster test --repeat 20 .mobster/checks/checkout-crash.yaml gives a pass rate. A crash that happens 1 run in 20 is a race, and the log of the failing run is the evidence. How to fix flaky XCUITests covers the usual races.
  • Turn on the sanitizers. For memory bugs, enable Address Sanitizer or Thread Sanitizer in the scheme and run the same check against that build.

Download Mobster for Mac to run these checks on your simulators and your own iPhones: free (AI runs on a supported AI plan, an API key or a model on your Mac).

Checks lists every field of the check file, and Device setup puts the helper on your iPhone.

Questions

  • Where are iOS crash reports on a Mac?

    A simulator's crash reports are in ~/Library/Logs/DiagnosticReports, and a connected iPhone's are in Xcode's Devices and Simulators window. TestFlight and App Store crashes are in Xcode's Crashes organizer and App Store Connect.

  • Why does my crash report show addresses instead of function names?

    The report is unsymbolicated. Xcode symbolicates it when it can find the dSYM for that exact build, so keep the dSYM of every build you ship.

  • Can Mobster fix a crash?

    No. It makes the crash repeatable and, on a simulator, attaches the evidence: the log, the stack with your own frames named and a video of the screens before it.

Sources