# 3.5 Using a debugger: breakpoints and stepping > Pausing a live Rust program with a debugger: breakpoints, stepping, watching variables, and the call stack, in VS Code and elsewhere. Source: https://learnrust.net/chapter-3/using-a-debugger/ 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](@/chapter-2/introduction-to-functions.md)'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](@/chapter-3/a-strategy-for-debugging.md) 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: ```rust 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. {% callout(kind="warning", title="Warning") %} Debug with the **dev profile** (plain `cargo build` defaults, which is also what the editor's Debug button uses). Lesson [0.10](@/chapter-0/build-configurations.md) 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. {% end %} {% callout(kind="author", title="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. {% end %} ## 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.