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.

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:

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

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 = 5

In 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

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 = 0

This 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 >= 18

Use 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
// c

This reduces friction and helps you build reliable debugging habits.

8. Limitations and Edge Cases

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

11. Practice Exercise

Use the following task to practice basic LLDB debugging in Swift.

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.