3.5Using a debugger: breakpoints and stepping

Last updated June 11, 2026

Print debugging has one structural limit: you must decide in advance where to look, edit the program, and rerun. A debugger removes the advance-notice requirement. It's a tool that runs your program under supervision, lets you pause it at any line, and, while paused, look at every variable and ask "now where, exactly, did this go wrong?" No code edits, no cleanup after.

The vocabulary

Five concepts cover essentially all debugger use, in every editor and language, for the rest of your career:

A breakpoint marks a line where execution should pause. Run the program; it stops just before executing that line, alive but frozen, everything inspectable.

Stepping advances the frozen program in controlled doses. Step over executes the current line and pauses at the next, treating any function call in it as one indivisible move. Step into follows the call instead, pausing at the first line inside the called function. Step out finishes the current function and pauses back at the caller. Between the three, you choose your altitude: skim main with step-over, descend into a suspect with step-into, climb back out when a function proves innocent.

Watching is reading variable values while paused: hover over a name in the source, or use the panel that lists every local variable in the current function, live.

And the call stack panel answers "how did the program get here?": the chain of calls currently in progress, your function on top, whoever called it beneath, down to main. Those are lesson 2.1's bookmarks, made visible: each entry is a function that's paused mid-call, waiting for the one above it to return. When a breakpoint inside a small helper trips, the call stack tells you which of its five callers is responsible for this particular visit.

That's the entire instrument panel. Watch yourself apply the lesson 3.3 strategy with it: breakpoint at a seam (one click instead of a dbg! line), run, read the values, and step onward in halves. Same hunt, live quarry.

Setting up, concretely

Debugger support comes from your editor plus a backend, and the pairing depends on platform. In VS Code: install CodeLLDB (extension ID vadimcn.vscode-lldb) on macOS and Linux, or Microsoft's C/C++ extension on Windows, alongside the rust-analyzer you already have. Then open any Rust project and look above fn main: rust-analyzer plants tiny Run | Debug links there (a "code lens"). Click Debug, and the program launches under the debugger; click in the left gutter beside any line number first to set a breakpoint (a red dot appears), and execution will pause there with the variables panel and call stack on the left.

RustRover users: the debugger is built in; the gutter dots and the Debug button work as above, no assembly required. Other editors have equivalents wired to the same concepts.

A practice program, purpose-built for a first session. Set a breakpoint on the let combined line, debug, then step into mix, watch its locals, check the call stack, and step out:

fn main() {
    let a = 10;
    let b = 32;
    let combined = mix(a, b);
    println!("{combined}");
}

fn mix(x: i32, y: i32) -> i32 {
    let sum = x + y;
    sum * 2
}

Ten minutes of poking at that program teaches more debugger than any page of prose; the goal of the session is for the five vocabulary words to become hand motions.

Warning

Debug with the dev profile (plain cargo build defaults, which is also what the editor's Debug button uses). Lesson 0.10 explained why: the dev profile carries the debug info that maps machine code to your lines, and skips the optimizer that rearranges them. A release build under a debugger steps "through" code in an order you won't recognize, skipping variables that no longer exist.

Author's note

Honesty boxes are for opinions, so: Rust's debugger story is solid, not stellar. The stepping/breakpoint/watch experience above works dependably, but C++'s flagship IDEs have richer integrations, and some Rust values display less readably than they should. The ecosystem leans correspondingly harder on compile-time errors, dbg!, and tests, and so does this course: I reach for the debugger maybe one bug in ten, when I want to wander through state rather than ask one question. Learn it, keep it holstered, and feel no guilt about loving dbg! more.

Quiz time

Question #1

You're paused at a breakpoint on let combined = mix(a, b);. What's the difference between step over and step into, right now?

Show solution

Step over executes the whole line (the call to mix included) and pauses at the println!. Step into follows the call, pausing at mix's first line, with x and y inspectable. Over = stay at this altitude; into = descend.

Question #2

A breakpoint inside read_number trips, and you need to know whether this is the first or second of the two calls main makes to it. Which panel answers that, and how?

Show solution

The call stack: it shows read_number on top and main beneath, with the line number in main the call came from. That line tells you which call site this visit belongs to.

Question #3

Why does debugging a --release build behave strangely?

Show solution

The release profile strips debug info (the code-to-source map the debugger needs) and optimizes aggressively, fusing and reordering lines and eliminating variables. The debugger can only show you the program that exists, which no longer matches the program you wrote.

The debugger and the print tools find bugs that exist. The chapter closes with the better deal: habits that keep bugs from existing.