LLDB Basics in Swift: Debugging Your Code in Xcode
LLDB is the debugger you use in Xcode to stop your Swift program, inspect values, step through code, and find bugs faster. This article teaches the core LLDB commands and workflows you need to debug Swift code with confidence.
Quick answer: LLDB lets you pause a running app, inspect variables, evaluate expressions, and control execution from Xcode’s debug console. In Swift, the most useful starter commands are po, p, bt, n, s, and c.
Difficulty: Beginner
You'll understand this better if you know: basic Swift syntax, how variables and constants store values, and the difference between running and paused code.
1. What Is LLDB Basics?
LLDB is the command-line debugger built into Xcode and the Swift toolchain. “LLDB basics” means the small set of actions you use most often while debugging: stop at a breakpoint, inspect state, step line by line, and print values.
- LLDB stands for the debugger used by Xcode for Swift apps.
- It runs while your program is paused, not while it is freely executing.
- You use it to inspect variables, objects, stack traces, and expression results.
- It helps you answer questions like “What value did this variable actually have here?”
In practice, LLDB is the bridge between your source code and the program state at a specific moment in time.
2. Why LLDB Basics Matter
Swift code often looks correct when you read it, but bugs usually come from unexpected runtime values, wrong control flow, or assumptions about data. LLDB helps you verify what is really happening instead of guessing.
It matters because it can shorten debugging time dramatically. Instead of adding temporary print statements, rebuilding repeatedly, and hoping you catch the issue, you can pause exactly where the problem occurs and inspect the live state.
LLDB is especially useful when:
- a value changes unexpectedly between function calls
- a crash happens only in one branch of logic
- you need to inspect a collection, optional, or object graph
- you want to understand how code flows through several functions
3. Basic Syntax or Core Idea
LLDB is used from the debug console while the app is paused. You type commands at the (lldb) prompt after hitting a breakpoint or pausing execution manually.
3.1 The simplest workflow
Set a breakpoint, run the app, and stop at the line you want to inspect. Then use LLDB commands to look around.
let name = "Ava"
let score = 42
print(name, score)If you pause here in Xcode, you can inspect name and score from the console.
3.2 Common starter commands
- po prints an object using Swift’s description rules.
- p prints a value and its type information.
- bt shows the current call stack.
- n steps over the current line.
- s steps into a function call.
- c continues execution until the next stop.
These commands are enough to debug many common Swift issues without leaving Xcode.
4. Step-by-Step Examples
4.1 Inspecting a variable with po
Suppose you want to see the current value of a string while stopped at a breakpoint.
let city = "Tokyo"
let message = "Welcome to \(city)"In the LLDB console, you can type po city to see the string value in a readable form. This is often the easiest way to inspect Swift values.
4.2 Seeing type information with p
Use p when you want both the value and type context.
let count = 5In the console, p count helps confirm that the value is what you expect, especially when type inference or bridging is involved.
4.3 Checking the call stack with bt
When something crashes or jumps to an unexpected line, the call stack tells you how execution got there.
func loadProfile() {
fetchUser()
}
func fetchUser() {
fatalError("Unexpected state")
}After the crash, bt shows the chain of function calls so you can find the path that led to the error.
4.4 Stepping through code with n and s
Stepping is useful when you want to watch a function run one line at a time.
func formatName(first: String, last: String) -> String {
let fullName = first + " " + last
return fullName
}n moves to the next line in the same function, while s enters a function call if one exists on the current line. That difference is important when you want to avoid or inspect a helper function.
4.5 Continuing after inspection with c
Once you finish checking state, continue running the app.
let items = ["tea", "coffee", "water"]If execution pauses here, you can inspect items, then type c to resume until the next breakpoint or crash.
5. Practical Use Cases
- Debugging a failed guard or unexpected optional unwrap.
- Inspecting array contents when a loop behaves differently than expected.
- Checking the order of function calls in a layered service or model flow.
- Verifying values passed into a network response handler.
- Finding the exact line where a crash occurs in a long call chain.
LLDB is most valuable when your bug depends on runtime data, not just syntax.
6. Common Mistakes
Mistake 1: Using print instead of pausing execution
Beginners often add print statements everywhere, then remove them later. That can work, but it is much less precise than pausing with a breakpoint and inspecting live state.
Problem: Print output can miss the exact moment a value changes, especially in asynchronous code or code that runs many times.
let value = 0
print("value is \(value)")Fix: Set a breakpoint and inspect the variable in LLDB instead of relying only on logging.
let value = 0This works better because LLDB shows the actual paused state at the exact line you care about.
Mistake 2: Stepping into the wrong place with s
Stepping into is useful, but it can take you deep into system code or helpers when you only want to move to the next line in your own function.
Problem: Using s when you meant n makes debugging slower and can be confusing because the debugger enters another function.
func makeLabel() -> String {
let text = formatName(first: "Sam", last: "Lee")
return text
}Fix: Use n when you want to stay in the current function and move line by line.
func makeLabel() -> String {
let text = formatName(first: "Sam", last: "Lee")
return text
}The corrected approach keeps you focused on your own code path.
Mistake 3: Evaluating expressions in the wrong context
LLDB can read local variables only when execution is paused inside the scope where those variables exist.
Problem: If you try to inspect a variable after execution has moved on, LLDB may say the variable cannot be found in the current context.
func calculateTotal() -> Int {
let subtotal = 12
let tax = 3
return subtotal + tax
}Fix: Pause inside the function before it returns, then inspect the variables while they are still in scope.
func calculateTotal() -> Int {
let subtotal = 12
let tax = 3
return subtotal + tax
}LLDB works best when you inspect state at the right time and in the right scope.
7. Best Practices
7.1 Use focused breakpoints
Instead of stopping everywhere, place breakpoints near the suspicious logic. That keeps the debugger fast and your attention on the right code path.
if items.isEmpty {
return
}Breaking only on the empty-case branch makes it easier to inspect why the code path was taken.
7.2 Prefer LLDB for live state, not for logic
LLDB is great for inspection, but it should not become part of your program logic. If you find yourself repeatedly evaluating the same condition in the console, move that logic into code or tests.
let isValid = age >= 18Use LLDB to confirm the result, but keep the real rule in Swift code where it can be tested.
7.3 Learn a small command set first
You do not need every LLDB command on day one. Start with a small set and use them consistently.
// Useful everyday commands in the console
// po variable
// p variable
// bt
// n
// s
// cThis reduces friction and helps you build reliable debugging habits.
8. Limitations and Edge Cases
- po and p can behave differently for Swift value types, bridged Foundation types, and custom descriptions.
- Some optimized release builds are harder to inspect because variables may be inlined or removed by the compiler.
- Asynchronous code can move on before you inspect it, so breakpoints inside tasks or completion handlers are often more useful.
- If a line never executes, a breakpoint there will never trigger; the bug may be earlier in the control flow.
- System frameworks and synthesized code can make stepping feel noisy, which is why line-focused breakpoints matter.
Problem: In optimized builds, LLDB may show missing or simplified variables because the compiler has changed the code for performance.
For debugging, prefer a debug configuration so the source code and runtime state line up more closely.
9. Practical Mini Project
Here is a small Swift example you can debug with LLDB. It calculates a checkout total and has a branch you can inspect with breakpoints.
struct Cart {
var items: [Int]
var discountCode: String?
func total() -> Int {
let subtotal = items.reduce(0, +)
let discount = discountCode != nil ? 5 : 0
return subtotal - discount
}
}
let cart = Cart(items: [10, 15, 20], discountCode: "SAVE5")
let result = cart.total()
print(result)Set a breakpoint inside total(), then inspect items, subtotal, and discount with po or p. Step through the calculation with n and confirm that the discount branch matches the data.
10. Key Points
- LLDB is the debugger you use in Xcode to inspect and control Swift code at runtime.
- po is great for readable value inspection, while p is useful when you want more type detail.
- bt helps you understand how execution reached the current point.
- n, s, and c are the core stepping commands.
- LLDB is most effective in debug builds and at carefully chosen breakpoints.
11. Practice Exercise
Use the following task to practice basic LLDB debugging in Swift.
- Create a function that calculates the average of three numbers.
- Set a breakpoint on the line where the sum is computed.
- Inspect each input value with p or po.
- Step over the calculation with n.
- Check the call stack with bt before continuing.
Expected output: The function should print the correct average for the numbers you choose.
Hint: Pause before returning the result so the temporary variables are still in scope.
Solution:
func average(a: Double, b: Double, c: Double) -> Double {
let sum = a + b + c
let result = sum / 3
return result
}
let value = average(a: 6, b: 9, c: 12)
print(value)This exercise reinforces the core LLDB flow: stop, inspect, step, and continue.
12. Final Summary
LLDB basics give you the practical debugging skills you need to inspect Swift code while it runs. Once you can pause execution, print values, read the call stack, and step through lines, you can solve a large class of bugs much faster.
The most important habits are simple: use breakpoints intentionally, inspect values in context, and choose the right stepping command for the job. As you get comfortable, LLDB becomes one of the fastest ways to understand why your Swift code behaves the way it does.
A good next step is to practice LLDB on a small Swift program with a few branches and an optional, then try reading the call stack and stepping through each path.