Interactive debugging with LLDB. The debugger freezes time so you can interrogate your running app — inspect variables, evaluate expressions, navigate threads, and understand exactly why something went wrong.
Core insight:
"LLDB is useless" really means "I don't know which command to use for Swift types." This is a knowledge-gap problem, not a tool problem.
Red Flags — Check This Skill When
Symptom
This Skill Applies
Need to inspect a variable at runtime
Yes — breakpoint + inspect
Crash you can reproduce locally
Yes — breakpoint before crash site
Wrong value at runtime but code looks correct
Yes — step through and inspect
Need to understand thread state during hang
Yes — pause + thread backtrace
po
doesn't work / shows garbage
Yes — Playbook 3 has alternatives
Crash log analyzed, need to reproduce
Yes — set breakpoints from crash context
Need to test a fix without rebuilding
Yes — expression evaluation
Want to break on all exceptions
Yes — exception breakpoints
App feels slow but responsive
No — use axiom-performance-profiling
Memory grows over time
No — use axiom-memory-debugging first
App completely frozen
Maybe — use axiom-hang-diagnostics first, then LLDB for thread inspection
Crash in production, no local repro
No — use axiom-testflight-triage first
LLDB vs Other Tools
digraph
tool_selection
{
"What do you need?"
[
shape
=
diamond
]
;
"axiom-testflight-triage"
[
shape
=
box
]
;
"axiom-hang-diagnostics"
[
shape
=
box
]
;
"axiom-memory-debugging"
[
shape
=
box
]
;
"axiom-performance-profiling"
[
shape
=
box
]
;
"LLDB (this skill)"
[
shape
=
box
,
style
=
bold
]
;
"What do you need?"
->
"axiom-testflight-triage"
[
label
=
"Crash log from field,\ncan't reproduce locally"
]
;
"What do you need?"
->
"axiom-hang-diagnostics"
[
label
=
"App frozen,\nneed diagnosis approach"
]
;
"What do you need?"
->
"axiom-memory-debugging"
[
label
=
"Memory growing,\nneed leak pattern"
]
;
"What do you need?"
->
"axiom-performance-profiling"
[
label
=
"Need to measure\nCPU/memory over time"
]
;
"What do you need?"
->
"LLDB (this skill)"
[
label
=
"Need to inspect state\nat a specific moment"
]
;
}
Rule of thumb:
Instruments
measures
. LLDB
inspects
. If you need to understand what's happening at a specific moment in time, use LLDB. If you need to understand trends over time, use Instruments.
Response Format
When helping with LLDB debugging, structure your output as:
Immediate diagnosis
(1-3 bullets, confidence-tagged: HIGH/MEDIUM/LOW)
Commands to run
(numbered, copy-paste ready, with
(lldb)
prefix)
What to look for
(command → expected output → interpretation)
Likely root causes
(ranked by probability)
Next breakpoint plan
(catch it earlier next time)
If no debugger attached
(crash-log-only fallback path)
Playbook 1: Crash Triage
Goal:
Understand why the app crashed, starting from the stop point.
Step 1: Read the Stop Reason
When the debugger stops, the first thing to check:
Translation: Main thread is waiting for a mutex lock. Something else holds it.
Main thread blocked on dispatch_sync:
frame #0: libdispatch.dylib`_dispatch_sync_f_slow
...
frame #3: MyApp`DataManager.fetchData()
Translation:
DispatchQueue.main.sync
called from background → classic deadlock.
Main thread busy (CPU-bound):
frame #0: MyApp`ImageProcessor.processAllImages()
frame #1: MyApp`ViewController.viewDidLoad()
Translation: Expensive work on main thread. Move to background.
Step 4: Check for Deadlocks
If two threads are both waiting on something the other holds:
(lldb) thread list
Look for multiple threads with state
waiting
that reference each other's locks.
Step 5: Inspect Specific Thread
(lldb) thread select 3
(lldb) bt
(lldb) v
Switch to another thread to inspect its state.
Cross-reference:
For fix patterns once you've identified the hang cause →
/skill axiom-hang-diagnostics
Playbook 3: Swift Value Inspection
This is the core value of this skill.
Most developers abandon LLDB because
po
doesn't work reliably with Swift types. Here's what actually works.
The Four Print Commands
Command
Full Form
What It Does
Best For
v
frame variable
Reads memory directly, no compilation
Swift structs, enums, locals —
your default
p
expression
(with formatter)
Compiles expression, shows formatted result
Computed properties, function calls
po
expression --object-description
Calls
debugDescription
Classes with
CustomDebugStringConvertible
expr
expression
Evaluates arbitrary code
Calling methods, modifying state
When to Use Each
Start with
v
— it's fastest and most reliable for stored properties:
(lldb) v self.userName
(lldb) v self.items[0]
(lldb) v localStruct
v
works by reading memory directly. It doesn't compile anything, so it can't fail due to expression compilation errors.
v
limitation:
It only reads stored properties — computed properties,
lazy var
(before first access), and property wrapper projected values (
$binding
) won't show meaningful values. If a field looks wrong or missing with
v
, try
p
instead.
Use
p
when
v
can't reach it:
(lldb) p self.computedProperty
(lldb) p self.items.count
(lldb) p someFunction()
p
compiles and executes the expression. Needed for computed properties and function calls.
Use
po
for class descriptions:
(lldb) po myObject
(lldb) po error
(lldb) po notification
po
calls
debugDescription
on the result. Best for objects that have meaningful descriptions (NSError, Notification, etc.).
The "LLDB Is Broken" Moments
What You See
Why
Fix
po
failed; variable hasn't been populated by optimizer
Use
v
instead
expression failed to parse, unknown type name
Swift expression parser can't resolve the type
Try
expr -l objc -- (id)0x12345
for ObjC objects, or use
v
Compiler optimized it out (Release build)
Rebuild with Debug, per-file
-Onone
, or
register read
as last resort
error: Couldn't apply expression side effects
Expression had side effects LLDB couldn't reverse
Try a simpler expression; avoid mutating state
po
shows memory address instead of value
Object doesn't conform to
CustomDebugStringConvertible
Use
v
for raw value, or implement the protocol
cannot find 'self' in scope
Breakpoint is in a context without
self
(static, closure)
Use
v
with the explicit variable name
p
shows
$R0 = ...
but
po
crashes
Different compilation paths
Use
p
when it works;
po
adds an extra description step that can fail
Inspecting Optionals
(lldb) v optionalValue
Shows:
(String?) some = "hello"
or
(String?) none
Don't use
po optionalValue
— it may show just
Optional("hello")
which is less useful.
Inspecting Collections
(lldb) v myArray
(lldb) v myArray[2]
(lldb) v myDict
For large collections, limit output:
(lldb) p Array(myArray.prefix(5))
Inspecting SwiftUI State
SwiftUI
@State
is backed by stored properties with underscore prefix:
(lldb) v self._isPresented
(lldb) v self._items
For
@Observable
models:
(lldb) v self.viewModel.propertyName
Diagnosing "view doesn't update":
If a property changes (confirmed with
v
) but the SwiftUI view doesn't re-render, check which thread the mutation happens on with
bt
.
@Observable
mutations must happen on
@MainActor
for SwiftUI to observe them — mutations on a background actor won't trigger view updates. Use
Self._printChanges()
inside a view body to see which property triggered (or didn't trigger) a re-render:
(lldb) expr Self._printChanges()
For the full observation diagnostic tree →
/skill axiom-swiftui-debugging
Inspecting Actors
Actor state is best inspected with
v
, which reads memory directly without isolation concerns:
(lldb) v actor
Shows all stored properties. This works because LLDB pauses the entire process — you can read any memory regardless of actor isolation (which is a compile-time concept).
This is the single most useful breakpoint for crash debugging. It stops at the throw site instead of the catch/crash site.
Watchpoints
Break when a variable's value changes:
(lldb) watchpoint set variable self.count
(lldb) watchpoint set variable -w read_write myGlobal
Watchpoints are hardware-backed — limited to ~4 per process but very fast.
One-Shot Breakpoints
Break once, then auto-delete:
(lldb) breakpoint set -f MyFile.swift -l 42 -o
Managing Breakpoints
(lldb) breakpoint list
(lldb) breakpoint disable 3
(lldb) breakpoint enable 3
(lldb) breakpoint delete 3
(lldb) breakpoint delete
Playbook 5: Async/Concurrency Debugging
Identifying Async Frames
Swift concurrency backtraces are noisy — expect
swift_task_switch
,
_dispatch_call_block_and_release
, and executor internals mixed in with your code. Don't be discouraged by 40+ frames of runtime noise. Focus on frames from YOUR module.
In Swift concurrency backtraces, look for
swift-task
frames:
Thread 3:
frame #0: MyApp`MyActor.doWork()
frame #1: swift_task_switch
frame #2: MyApp`closure #1 in ViewController.loadData()
The
swift_task_switch
frame indicates an async suspension point. Your code frames are the ones prefixed with your module name (
MyApp
above).
Inspecting Task State
(lldb) thread backtrace all
Look for threads with
swift_task
in their frames. Each represents an active Swift task.
Actor-Isolated Code
When stopped inside an actor:
(lldb) v self
Shows all actor state. This works because LLDB pauses the entire process — actor isolation is a compile-time concept, not a runtime lock (for default actors).
Task Group Inspection
When debugging task groups, break inside the group closure and inspect:
(lldb) v
(lldb) bt
Each child task runs on its own thread. Use
bt all
to see them.
Cross-reference:
For Swift concurrency patterns and fix strategies →
/skill axiom-swift-concurrency
. For profiling async performance →
/skill axiom-concurrency-profiling
Pressure Scenarios
Scenario 1: "Release-Only Crash — LLDB Is Useless in Release"
Situation:
Crash happens in Release builds but not Debug. Team says "we can't debug it."
Why this fails:
Release optimizations change timing, memory layout, and can eliminate variables — making the crash non-reproducible in Debug.
Correct approach:
Build with Debug configuration but Release-like settings:
Optimization Level:
-O
(not
-Onone
)
Still include debug symbols (
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
)
Enable Address Sanitizer (
-fsanitize=address
) — catches memory errors with 2-3x overhead
Use the crash report to set breakpoints at the crash site
Set exception breakpoints to catch the error before the crash:
(lldb) breakpoint set -E swift
(lldb) breakpoint set -E objc
If variable shows
, reduce optimization for that one file:
Build Settings → Per-file flags →
-Onone
for the specific file
Last resort — read register values directly (variables live in registers before being optimized out):
(lldb) register read
(lldb) register read x0 x1 x2
On ARM64:
x0
= self,
x1
-
x7
= first 7 arguments. Check
/skill axiom-lldb-ref
Part 1 for details.
Scenario 2: "Just Add Print Statements"
Situation:
Developer adds
print()
calls to debug, rebuilds, runs, reads console. Repeat.
Why this fails:
Each print-debug cycle costs 3-5 minutes (edit → build → run → navigate to state → read output). An LLDB breakpoint costs 30 seconds.
Correct approach:
Set a breakpoint at the line you'd add a
print()
:
(lldb) b MyFile.swift:42
Add a logpoint for "print-like" behavior without rebuilding:
One breakpoint session replaces 5-10 print-debug cycles.
Time comparison
(typical control-flow debugging):
Approach
Per investigation
5 variables
print() statements
3-5 min (build + run)
15-25 min
LLDB breakpoint
30 sec (set + inspect)
2.5 min
Exception:
In tight loops (thousands of hits/sec), logpoints add per-hit overhead. Use
-i
to skip to the iteration you care about, or use a temporary
print()
for that specific loop.
Scenario 3: "po Doesn't Work So LLDB Is Broken"
Situation:
Developer types
po myStruct
and gets garbage. Concludes LLDB is broken for Swift. Goes back to print debugging.
This is the #1 reason developers abandon LLDB.
Why
po
fails with Swift structs:
po
calls
debugDescription
which requires compiling an expression in the debugger context. For Swift structs, this compilation often fails due to missing type metadata, generics, or module resolution issues.
Correct approach:
Use
v
instead of
po
— reads memory directly, no compilation:
(lldb) v myStruct
(lldb) v myStruct.propertyName
Use
p
for computed properties:
(lldb) p myStruct.computedValue
Use
po
only for classes with
CustomDebugStringConvertible
If
p
also fails, try specifying the language:
(lldb) expr -l objc -- (id)0x12345
If everything fails,
v self
always works inside a method.
Anti-Patterns
Anti-Pattern
Why It's Wrong
Better Alternative
po
everything
Fails for Swift structs, enums, optionals
v
for values,
po
only for classes
Print-debug cycles
3-5 min per cycle vs 30 sec breakpoint
Breakpoints with logpoint actions
"LLDB doesn't work with Swift"
It does — wrong command choice
v
is designed for Swift values
Ignoring backtraces
Jumping to guesses instead of reading the trace
bt
first, then navigate frames
Conditional breakpoints on every hit
Slows execution if condition is expensive
Use
-i
(ignore count) when possible
Debugging optimized (Release) builds
Variables missing, code reordered
Debug configuration, or per-file
-Onone
Force-continuing past exceptions
Hides the real error
Fix the exception, don't suppress it
No exception breakpoints set
Crashes land in system code, not throw site
Always add Swift Error + ObjC Exception breakpoints