# Using a Debugger (Breakpoints, Stepping & Watch)

> What a debugger actually is - a way to pause your program mid-run and inspect everything that's true at that instant - and how breakpoints, stepping, and watch expressions transfer across every IDE, language, and browser devtools.


---

# Using a Debugger (Breakpoints, Stepping & Watch)

You already know how to debug with `print()`. You add a line, run the program, read the output, guess
what's wrong, add another line, run it again. It works - until the bug only shows up after twenty
iterations, or only when three values line up just so, and each "run it again" costs you a minute and a
fresh dose of frustration.

There's a tool that's been sitting in your editor the whole time, and most people never reach for it
because nobody showed them what it actually does. A debugger lets you **freeze your program mid-run and
look at everything that's true at that exact instant** - every variable, every function that called the
one you're in, the real value of that thing you've been guessing about. No re-running. No guessing. You
just look.

This guide teaches the debugger as a set of ideas, not as a tour of one IDE's buttons. The moves are the
same in VS Code, PyCharm, IntelliJ, GDB, and your browser's devtools - once you understand them, they
follow you everywhere.

## How to read this

- **Want to know if it's even worth it?** Start with [Phase 1: Why a Debugger Beats print()](01-why-a-debugger-beats-print.md)
  - it's straight about when print debugging is still the right call.
- **Want it to finally make sense?** Read in order. Phase 2 teaches the universal controls; Phase 3 levels
  you up to the moves that solve the bugs `print()` can't touch.

## The phases

1. **[Why a Debugger Beats print()](01-why-a-debugger-beats-print.md)** - the mental model: a debugger
   *pauses* your program and lets you inspect reality, instead of guessing and re-running. When that saves
   you hours (and when `print()` is genuinely fine).
2. **[The Core Moves](02-the-core-moves.md)** - the universal controls every debugger has: breakpoints,
   step over / into / out, inspecting variables and the call stack, and watch expressions. Explained so
   they transfer across tools.
3. **[Debugging for Real](03-debugging-for-real.md)** - conditional breakpoints, watchpoints, debugging
   across the stack (backend in your IDE, frontend in devtools), and reading the call stack at a
   breakpoint - plus the gotcha where a breakpoint changes the timing of a race condition.

> This guide deliberately stops at the moves that work in every debugger. Tool-specific superpowers -
> time-travel debugging, remote debugging into a container, core-dump analysis - are a deeper topic for a
> follow-up guide. Master the universal moves here first; they're 90% of what you'll ever need.

Related reading: [Reading a Stack Trace](/guides/reading-a-stack-trace) and
[How to Reproduce a Bug](/guides/how-to-reproduce-a-bug) - a debugger is far more powerful once you can
reliably trigger the bug and read where it blew up.


---

# Why a Debugger Beats print()

Picture your usual debugging loop. Something's wrong, so you sprinkle in `print()` calls - `print("here")`,
`print(user)`, `print("got past the check")` - run the program, squint at the output, form a theory, and add
more prints to test it. Each lap costs a code edit, a re-run, and patience. When the bug only shows on the
200th loop, or only when two unrelated values collide, that loop eats an afternoon. There's a way to skip it
entirely - not the buttons yet, but *why the whole approach is different*.

## What a debugger actually is

**The mental model.** A debugger **pauses your program while it's running** and hands you a frozen snapshot
of that instant: every variable in scope, every caller that got you here. Ask "what is `total` right now?"
and get the real answer, not a stale `print` from three runs ago.

`print()` debugging is taking a photo, walking away, developing the film, and only then seeing what you
captured - point the camera wrong and you start over. A debugger is standing in the room with the lights
frozen, free to look anywhere.

**Why people get this wrong.** Many treat the debugger as "the advanced thing" and reach for `print()`
reflexively even when it's slower. It's the opposite: the debugger usually wins with *less* effort, because
you stop editing-and-rerunning and start *looking*.

**What it does in real life.** Mark a line where the program should stop, then run it. Execution halts there
and your editor shows the current state. Poke around, run one more line, poke around again - full visibility
instead of guessing where to bolt on an observation point.

📝 **Breakpoint.** The line you mark as "stop here." The program runs normally until it hits that line, then
pauses *before* running it, handing you control. More in [Phase 2](02-the-core-moves.md).

## Where print() quietly fails you

The cost of `print()` is small frictions that add up:

- **You must predict what to look at.** Every `print()` is a bet placed before you knew the answer; guess
  wrong and re-run to place a new one.
- **The edit-rerun tax.** Each new question means changing the source and restarting - brutal for a
  slow-booting server or a minute-long test.
- **It only sees what you named.** `print(user)` shows `user`, not the variable two frames up you didn't
  think to print.
- **The cleanup.** You'll eventually commit a stray `print("HERE!!!")` to a pull request.

A debugger removes the bet: when paused, *everything* in scope is visible at once, and asking a new
question is a glance, not a re-run.

## When this saves you hours

The debugger pulls ahead in exactly the situations that make `print()` miserable:

- **The bug is deep in a loop or recursion.** "Wrong on some iteration" means scrolling 200 lines of
  `print()` output. A breakpoint that only fires on the bad iteration drops you into the moment it goes
  wrong. (How, in [Phase 3](03-debugging-for-real.md).)
- **You don't know where the bug is.** Pause early and *walk* forward, watching values change until one goes
  wrong - no guessing which line to print near.
- **The state is large or nested.** A request object, a nested config, a thirty-field ORM model - printing
  that is noise. A debugger expands it like a folder; read only what you need.
- **Re-running is expensive.** Slow startup, a hard-to-reproduce setup, a ten-click flow to the bug - pause
  once, ask every question from that stop.

## When print() is still fine

A guide that says *always* use the debugger would be lying. Print and log debugging is respectable, and
sometimes the *better* tool:

- **You already have a strong hunch.** 90% sure `value` is `None` on line 40? A single `print(value)`
  confirms it faster than launching a debug session.
- **The bug spans many runs or lives in production.** A debugger pauses *one* execution. To spot a pattern
  across thousands of requests, or a failure you can't reproduce locally, structured **logging** wins - logs
  persist and aggregate; you can't attach a breakpoint to a server you can't reach.
- **Timing-sensitive and concurrent code.** Pausing changes *when* things happen and can make a race
  condition vanish. Logging observes without stopping the clock. (More in [Phase 3](03-debugging-for-real.md).)
- **The debugger isn't practical here.** Minified code with no source maps, a tangled build, an unreachable
  environment - a log line is sometimes the path of least resistance, and that's okay.

💡 **Key point.** The skill isn't "always use the debugger" - it's *knowing which tool the situation calls
for*. Most developers avoid it only because nobody walked them through it. After the next two phases,
you'll pick on the merits.

## Recap

1. A debugger **pauses your running program**, showing everything true at that instant - no guessing, no
   re-running.
2. `print()` forces you to *predict* what to observe and pay an edit-rerun tax per question; a breakpoint
   shows *all* state at once.
3. The debugger wins big on deep loops, unknown bug locations, large nested state, and expensive re-runs.
4. `print()` / **logging** still wins for strong hunches, patterns across many runs, production, and
   timing-sensitive code.

Now that you know *why*, let's learn the handful of controls that work in every debugger.


---

# The Core Moves

Open any debugger - VS Code, PyCharm, IntelliJ, Chrome devtools, GDB - and you'll find the same controls
wearing different paint. Maybe six moves total; once they click, every debugger feels familiar. This phase
teaches them as *ideas*, not buttons to relearn per tool.

One example runs throughout: a function meant to total a shopping cart but returning the wrong number.

```python
def cart_total(items):
    total = 0
    for item in items:
        total += item.price * item.quantity
    return apply_discount(total)

def apply_discount(amount):
    return amount * 0.9
```

## The breakpoint - "pause here"

**What it actually is.** A breakpoint tells the debugger: *stop right before this line and give me control.*
Set one by clicking the margin left of the line number - a red dot appears. The program runs full speed
until it reaches that line, then freezes.

**What it does in real life.** Put a breakpoint on `return apply_discount(total)` and run in debug mode: the
loop runs, then execution stops with `total` holding whatever the loop produced.

**A real example.** A paused session in a typical debug console (layout varies, but every debugger shows
where you're paused, local values, and the call stack):

```console
Paused on breakpoint at cart.py:5

VARIABLES
  Locals:
    items    = [Item(price=10, quantity=2), Item(price=5, quantity=1)]
    total    = 25
    item     = Item(price=5, quantity=1)

CALL STACK
  ▶ cart_total      cart.py:5
    handle_checkout web.py:88
    <module>        web.py:140
```
*What just happened:* The loop ran and stopped *before* calling `apply_discount`. `total` is `25` right now
- no `print()` required, sitting alongside every other local.

⚠️ **Gotcha - run in "debug" mode, not normal run.** A breakpoint only fires with the debugger *attached*
(the "Debug" / bug-icon button, `python -m pdb`, `node --inspect`). Plain "Run" does nothing - not broken,
just not invited.

## Inspecting variables - "what's true right now?"

**What it actually is.** While paused, the debugger shows every variable in scope and its value. Objects
expand like folders, click to drill in - the payoff from Phase 1: instead of betting on what to print, you
see *all* the local state at once.

**What it does in real life.** In the snapshot above, read `total = 25` and expand `items` to inspect each
`Item` - if `total` looks wrong, you've found *where*, no re-running needed. Most debuggers also give an
"evaluate" / "console" panel: type `items[0].price * items[0].quantity` for the answer *in the live paused
context* - a calculator running inside your frozen program.

## Stepping - moving forward one piece at a time

Once paused, you control how the program moves forward. Three step commands exist, and the difference
between them is the single most useful thing in this guide.

📝 **The current line.** When paused, there's always one "next line to run." Stepping decides how much of
it - and what's inside it - runs before control returns to you.

**Step over - "run this line, don't show me the details."**
Runs the current line completely, *including any function it calls*, and pauses on the next line, same
level. Use when you trust the called function and just want the result.

```console
# Paused at:  total += item.price * item.quantity   (line 4)
> step over
# Now paused at: total += item.price * item.quantity (line 4, next loop iteration)
#   total = 25
```
*What just happened:* One iteration ran, landing on the same line for the next pass - `total` went from
`20` to `25`, without diving into the multiplication.

**Step into - "take me inside the function being called."**
Descends into a function you want to inspect, pausing on its first line - how you follow the bug into a
helper.

```console
# Paused at:  return apply_discount(total)   (line 5)
> step into
# Now paused at: return amount * 0.9         (apply_discount, line 8)
#   amount = 25
```
*What just happened:* Instead of running `apply_discount` invisibly, you climbed *inside* it and can see its
argument (`amount = 25`) compute. Answers "is the bug here, or in the caller?"

**Step out - "I've seen enough in here, finish this function and pop me back up."**
Runs the rest of the function, pausing at the caller. Use once you've confirmed the bug isn't here.

```console
# Paused inside apply_discount (line 8)
> step out
# Now paused back in cart_total, at the line after the call
#   return value = 22.5
```
*What just happened:* `apply_discount` finished, returned `22.5`, and dropped you back at the call site,
skipping the rest of the helper without losing your place.

⚠️ **Gotcha - step into can drop you into library code.** Stepping into a line that calls *framework* or
*standard-library* code can pause you inside unfamiliar source, several levels deep. Fix: step *out*, or
"step over" library-only lines. Many debuggers offer a "just-my-code" setting that prevents this - worth
turning on.

## The call stack - "how did I get here?"

**What it actually is.** The call stack is the chain of calls that led here - each function "waiting" for the
one below it to return. In the earlier snapshot:

```mermaid
flowchart TD
  mod["&lt;module&gt;  ← top-level code that started it all"] -->|called| hc["handle_checkout"]
  hc -->|called| ct["cart_total  ← you are here (top of the stack)"]
```

**What it does in real life.** Click any frame and the debugger jumps to that function's line, showing its
variables *at the moment it made the call below it*. If `cart_total` got a weird `items` list, click
`handle_checkout` to see what it passed in - without re-running. Same structure as a crash; if reading
frames feels shaky, [Reading a Stack Trace](/guides/reading-a-stack-trace) covers it in depth.

💡 **Key point.** Variables answer *what is true here*; the call stack answers *how did we get here*. Most
real bugs need both.

## Watch expressions - "keep an eye on this for me"

**What it actually is.** A watch expression is code pinned to the debugger that re-evaluates *every pause* -
add `total / len(items)` once and it stays updated automatically, instead of expanding `items` and doing
mental math each stop.

**What it does in real life.** Watching `item.price * item.quantity` while stepping through the loop shows
that product change every iteration, so the moment it goes wrong jumps out. A watch can be any valid
expression: a variable, a calculation, a method call, a comparison like `total > 100`.

⚠️ **Gotcha - watch expressions can have side effects.** A watch *runs* its expression every pause, so
watching `cache.pop(key)` or `next(iterator)` mutates state each time - quietly changing the thing you're
debugging. Keep watches *read-only*; for side effects, use the evaluate/console panel once instead.

## The whole picture

The mental model of a paused session, every move you've learned in one frame:

```text
  ┌─────────────────────────────── PAUSED ───────────────────────────────┐
  │                                                                       │
  │   cart.py                                                             │
  │     1  def cart_total(items):                                         │
  │     2      total = 0                                                  │
  │     3      for item in items:                                         │
  │  ●  4          total += item.price * item.quantity   ◄── current line │
  │     5      return apply_discount(total)                               │
  │     ●  = breakpoint                                                   │
  │                                                                       │
  │   VARIABLES (what's true right now)   CALL STACK (how we got here)    │
  │     total = 20                          ▶ cart_total      cart.py:4   │
  │     item  = Item(price=5, qty=1)          handle_checkout web.py:88   │
  │     items = [Item, Item]                  <module>        web.py:140  │
  │                                                                       │
  │   WATCH (re-checked on every pause)   CONTROLS                        │
  │     item.price * item.quantity = 5      step over → run line, stay    │
  │     total > 100                = False  step into → go inside a call  │
  │                                         step out  → finish & pop up   │
  └───────────────────────────────────────────────────────────────────── ┘
```

Every debugger is some arrangement of these five regions. Learn them once, recognize them everywhere.

## Recap

1. A **breakpoint** pauses the program *before* a line runs - only in debug mode.
2. **Inspecting variables** shows all live state at once; an evaluate box runs expressions in context.
3. **Step over** runs a line whole; **step into** descends into a call; **step out** finishes the function
   and pops up a level.
4. The **call stack** is the chain of callers - click a frame to see *its* variables and how you got here.
5. A **watch expression** re-evaluates on every pause - keep it read-only.

You can now drive any debugger through a normal bug. Next: the moves that crack bugs `print()` can't touch.

Watch it animated: [using breakpoints](/explainers/Breakpoints.dc.html)


---

# Debugging for Real

This phase covers moves that turn the debugger from "a nicer print()" into a tool that solves bugs nothing
else can: pausing at the exact moment things go wrong, catching the instant a value changes, following one
action from a click into your backend. We close with the one case where the debugger can *lie* to you.

## Conditional breakpoints - "pause only when X is true"

**What it actually is.** A normal breakpoint pauses *every* time; a conditional one only when your condition
evaluates true. Set one by right-clicking a breakpoint and entering an expression (most IDEs: "Edit
Breakpoint" → Condition). Recall the "200th loop iteration" misery from Phase 1, where a plain breakpoint
forces 199 clicks of "continue" - a conditional one with `item.id == 4096` fires *once*, right when the loop
hits the culprit.

**A real example.**

```console
# Breakpoint on:  total += item.price * item.quantity
# Condition:      item.quantity < 0

Paused on breakpoint at cart.py:4  (condition met: item.quantity < 0)
  item  = Item(price=10, quantity=-3)
  total = 140
```
*What just happened:* The loop ran full speed through every item, stopping only at the negative quantity - a
needle found without touching the haystack.

💡 **Key point.** The condition evaluates in the paused context with normal operators - high-value patterns:
`user.id == 42` (one user), `count > 1000` (past a threshold), `result is None` (the failing case). Also
worth knowing: a **hit count** condition ("pause on the 50th time this line runs") covers knowing *how many*
iterations in but not *which value* causes it.

## Watchpoints - "pause when this value changes"

**What it actually is.** A breakpoint is tied to a *line of code*; a watchpoint ("data breakpoint") is tied
to a *piece of data* - it pauses the instant a variable or field *changes*, no matter which line did it. This
solves the worst bug: "this value is wrong by the time I look, but I have no idea where it got set" - instead
of `print()` scattered across dozens of suspect lines, tell the debugger *what* to watch and it tells you
*where* it changed.

**What it does in real life.** Set a watchpoint on `account.balance`. The moment any code assigns it a new
value, execution freezes on that line, and the call stack shows who did it.

```console
Watchpoint hit: account.balance changed
  old value: 500
  new value: -120
Paused at billing.py:73  in apply_refund()
```
*What just happened:* No guessing which function corrupted the balance - the debugger stopped on the exact
line that wrote it. A day-long bug, solved in one stop.

⚠️ **Gotcha - watchpoint support varies.** GDB, LLDB, and many IDEs support them well, but availability
depends on language and platform: some watch only a fixed number at once (often tied to CPU hardware
support), some interpreted-language debuggers offer none (fall back to a conditional breakpoint instead). If
you don't see the option, that's why.

## Debugging across the stack

Modern bugs love the seam between frontend and backend: the click sending the wrong payload, the API
returning the wrong shape, the response the UI mishandles. The power move: debug *both sides of the same
action at once*, running two debuggers in parallel:

```mermaid
sequenceDiagram
  participant B as Browser devtools<br/>(frontend pause)
  participant S as IDE / server<br/>(backend pause)
  Note over B: breakpoint in click / fetch code
  B->>S: HTTP request
  Note over S: breakpoint in route / controller
  S-->>B: JSON response
```

- **Frontend:** in devtools' **Sources** panel, breakpoint where the click handler sends the request - same
  Phase 2 moves (breakpoints, stepping, variables, call stack, watches), just in browser clothing.
- **Backend:** attach your IDE's debugger to the server and breakpoint the route that receives it.

**What it does in real life.** Trigger the action: the *frontend* breakpoint fires first, showing the
payload about to be sent. Continue and the *backend* breakpoint catches the request, showing what arrived
and how it's read. The bug - "UI sends `userId`, API reads `user_id`" - becomes obvious once you see both
sides.

📝 **Source maps.** Frontend code is usually bundled and minified, so the browser shows unreadable
one-letter-variable soup. *Source maps* are build-generated files mapping the bundle back to your original
source - gibberish in devtools means they aren't served, fix that first.

## Reading the call stack at a breakpoint

You met the call stack in Phase 2 as "how did I get here" - deep in real code it's your primary navigation
tool, same skill as reading a crash: top-to-bottom, newest frame first, click down to the frame that
*actually* made the bad decision.

```mermaid
flowchart TD
  mod["&lt;module&gt;  web.py:140  ← where it all started"] -->|called| disp["dispatch  web.py:51"]
  disp -->|called| hc["handle_checkout  web.py:88"]
  hc -->|"called cart_total(cart.items)"| ct["cart_total  cart.py:5"]
  ct -->|"called apply_discount(total)"| ad["apply_discount  cart.py:8  ← paused; amount looks wrong"]
```

Paused in `apply_discount` with a wrong `amount`? The value came from above. Click `cart_total` to see
`total` at the call; click `handle_checkout` to see what `cart.items` looked like when *it* called in -
walking *backward* up the chain of causes until reality first diverged from what you expected.

> This is the live version of [Reading a Stack Trace](/guides/reading-a-stack-trace): a crash hands you a
> *dead* stack printed after the fact, a breakpoint a *live* one you can click through - same structure, far
> more power. You need the bug on demand first, though - [How to Reproduce a Bug](/guides/how-to-reproduce-a-bug)
> is the prerequisite for putting a breakpoint anywhere useful.

## ⚠️ The big gotcha: a breakpoint changes the timing

The one that humbles everyone eventually: a debugger doesn't observe your program like a camera - to pause
one part, it *stops the clock* on it, and in timing-sensitive code that can change the outcome.

**Where this bites: race conditions.** Say two threads race to update a shared counter, and the bug is that
both sometimes read it before either writes. You set a breakpoint - but while that thread sits paused, the
*other* keeps running (or, depending on settings, also stops). Either way you've changed their relative
timing, and the interleaving that caused the bug may now never happen: it *disappears the moment you look
for it*, nicknamed a **heisenbug** after the physicist's uncertainty principle.

For timing-sensitive and concurrent bugs, lean on tools that *don't* stop the clock:

- **Logging**, not breakpoints. Timestamped, thread-ID'd log lines run the program at full speed, showing
  the real interleaving unaltered - one case where Phase 1's logging genuinely beats the debugger.
- **Conditional logpoints.** A "logpoint" is a breakpoint that *logs and keeps going* instead of pausing -
  no-code-edit logging without freezing execution.
- **Purpose-built tools.** Thread sanitizers and race detectors catch these without a human watching live.

💡 **Key point.** The debugger is near-perfect for *sequential* logic, where pausing changes nothing, but an
unreliable witness for *concurrency and timing*, since pausing is itself an intervention. Know which kind of
bug you have before trusting what the breakpoint shows you.

## Recap

1. **Conditional breakpoints** pause only when your expression is true - cures "the bug is on some
   iteration"; hit-count conditions cover "pause on the Nth time."
2. **Watchpoints** pause when a *value* changes, showing *where* it got set, where supported.
3. **Debug across the stack**: a breakpoint in browser devtools (Sources panel) plus one in your backend,
   following one action across the boundary. Serve source maps so frontend code is readable.
4. At a breakpoint, **walk the call stack backward** - click each caller's frame until you find where
   reality first went wrong.
5. ⚠️ A breakpoint **changes timing** - for race conditions and concurrency, reach for logging, logpoints, or
   race detectors instead.

You now have the universal debugger toolkit: pause precisely, inspect everything, step with intent, navigate
the stack, and know when it's the wrong tool - the judgment separating print-debugging from real debugging.
