Swift debugging techniques: breakpoints, runtime inspection, and crash triage

This article focuses on everyday debugging tools that still matter: logging, regular and conditional breakpoints, non-stopping breakpoint actions, `po` in LLDB, and Xcode Organizer when the crash already happened in the wild.

Xcode creating a regular breakpoint by clicking a line number

This article is useful because it moves from the simplest debugging habit to the more structured Xcode tools without pretending every problem needs the same level of machinery.

Sometimes you only need a quick log. Sometimes you need the debugger to stop on the exact iteration that matters. And sometimes the crash has already escaped into production, so the only practical starting point is Organizer. This article keeps those layers separate so the workflow stays easy to reason about.

Example app The source examples use an array named myCats and a simple loop over each cat, which makes the breakpoint and LLDB examples easy to follow.

Start with logging when the bug is still small enough that plain output can explain the control flow.

This article starts with the oldest debugging tool for a reason: it is still fast. If you only need to verify that a collection contains what you expect or that a loop is visiting the right values, a few temporary prints can be enough.

func dailyRoutine() {
    print(myCats)

    myCats.forEach { cat in
        print("Current cat: \(cat.name)")
        cat.feed()
        cat.hug()
    }
}

Logging is not elegant, but it is cheap. The tradeoff is that once you need to stop on one specific state instead of printing everything, breakpoints become a better tool.

Regular breakpoints stop the program at one exact line, while conditional breakpoints wait until the state actually becomes interesting.

A normal breakpoint is just a stop marker on a line number. Run the app, let execution hit that line, and Xcode freezes the process so you can inspect the local state instead of inferring it from logs.

Creating a standard breakpoint in Xcode
A regular breakpoint is the fastest way to stop on a known line.
Xcode paused at a breakpoint during program execution
Once execution pauses, the debugger view becomes much more useful than additional print statements.

Conditional breakpoints are the next step up. Instead of stopping on every loop iteration, they stop only when a condition becomes true.

Editing a breakpoint to make it conditional
Conditional breakpoints filter out the noisy iterations automatically.
Conditional breakpoint dialog with an example expression
The article's example is a name check such as cat.name == "Nekonohi".

Breakpoint actions let you log or evaluate expressions without stopping execution, which is often cleaner than scattering temporary print statements across the code.

This article uses this feature to turn a breakpoint into a structured trace point. You attach an action such as po cat.name and tell Xcode to continue automatically after evaluation.

Adding a debugger action to a breakpoint
Breakpoint actions combine the precision of a breakpoint with the low interruption cost of logging.
Breakpoint editor showing action fields and continue automatically
Enable automatic continue if you want output in the console but do not want execution to pause.
Console output from a breakpoint action printing each cat name
The result is a targeted stream of values without manually editing the source file just to print them.

Once the process is paused, the built-in debugger console becomes the most flexible tool in the article: you can inspect values, derive new information, and even mutate state.

The post uses the po command, which prints the result of an expression from the current runtime context. That makes it ideal for answering focused questions while paused at a breakpoint.

Breakpoint location used before running po commands in the Xcode console
Pause first, then use the console as a live inspection surface.
po myCats
po myCats.count
po myCats.first?.name
Using po to inspect a collection at runtime
Inspect the whole collection when you need structure, not just one scalar value.
Using po to inspect a collection count in the debugger
Inspect derived values like counts when the full object graph is too noisy.
Using po to inspect the first cat name in the debugger
Properties and optional chains are fair game too.

The article also points out that the debugger can mutate state. That is not a production fix, but it is a good way to test a theory instantly.

po myCats.count      // 2
po myCats.append(.init())
po myCats.count      // 3
Using po to mutate runtime state and verify the result
Runtime mutation is powerful, but it is best used as a debugging probe rather than a habit.

When the bug already escaped into a shipped build, Organizer becomes the article's production-side entry point for crash investigation.

If the app is already on the App Store, Apple can aggregate crash reports for you. The article walks through the Organizer path: open Window > Organizer, choose the app, open the Crashes section, pick a version, then inspect one concrete report.

Opening Organizer from the Xcode Window menu
Organizer is the handoff point from local debugging to post-release crash investigation.
Organizer showing the Crashes section for an app
Select the app first, then drill into the crash list for the right release.
Organizer version selector for crash reports
Choose the version before drawing conclusions from a crash cluster.
Crash detail view in Organizer
The useful end state is the crash detail view, especially when you can jump back into the project from there.

This article closes by reminding the reader to check metrics too, especially memory-related signals, because not every production problem first appears as a clean crash.

The article still holds up because it does not treat debugging as one tool or one screen. It is a ladder.

Start with logs when the question is simple. Use breakpoints when timing matters. Add conditions when only one state is relevant. Use breakpoint actions when you need structured output without stopping. Move into po when the program is paused and you want to interrogate the real runtime. And when the bug only exists in shipped builds, switch to Organizer and work backward from the crash report.