Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What the Debugger Cannot Show You

A debugger is an instrument for observing state. You pause execution at a point in time and look: what are the values in memory? What is on the call stack? What is the value of this variable at this moment?

This is genuinely powerful. Before debuggers, you inferred state from printed output or, earlier still, from blinking lights. The ability to halt a running program and examine it directly is a significant advance in the practical epistemology of programming.

But there is something the debugger structurally cannot show you, and understanding that gap changes how you use it.


State Is Not Causality

When your program fails, you want to know why. The debugger shows you what — the value that is wrong, the nil pointer, the off-by-one index. It does not, by itself, show you why that value is there.

This distinction matters because bugs live in causality, not state. The wrong value at line 247 is the symptom. The cause is somewhere earlier in the execution — a decision made at line 12, a value calculated at line 89, an assumption in a function called three frames up the stack. The debugger shows you the failure site. The cause may be far removed.

Experienced debuggers know this implicitly. They look at the wrong value and ask: how did this get here? They walk up the call stack, set breakpoints earlier in the execution, trace the value backward through the code. They are using the debugger as a starting point for a causal investigation, not as a direct answer.

Less experienced debuggers often stop at the failure site. The program crashed on line 247, so there must be something wrong with line 247. They read line 247 carefully, they look at the values there, they try to understand what went wrong at that point. Sometimes this is sufficient. Often it is not, because the line is just where the consequence became visible.


Time and the Single Frame

A debugger shows you a frame: the state of the program at a particular instant. Even with logging and traces, you are looking at snapshots. The full execution history — every mutation, every conditional, every function call that shaped the current state — is typically not available.

Some tools try to address this. Reversible debuggers like rr on Linux allow you to record execution and step backward in time, examining past states. Omniscient debuggers store the entire execution trace. These are genuinely useful advances.

But even with full execution history, you still have the problem of interpretation. The trace tells you what happened, in sequence. It does not tell you what it means. Understanding causality still requires a mental model — a theory about how the code is supposed to work, against which the actual behavior can be compared.

A full execution trace given to an engineer who does not understand the system is mostly noise. The same trace given to an engineer who has a clear model of what the system should be doing becomes meaningful. The tool is the same. What differs is the model.


The Heisenberg Problem

There is a version of the observer effect in debugging that every developer has encountered: the bug disappears when you add logging to find it.

This is not magic. It has several mundane explanations. The logging adds timing — a race condition resolves differently when execution is slower. The logging changes memory layout. The act of compiling with debug symbols or without optimization changes behavior. The bug was triggered by a specific scheduling or interrupt sequence that the presence of new code disrupts.

The deeper issue is that debugging is not purely observational. When you instrument a system, you change it. The debugger is not a neutral window onto a running program — it is a probe that interacts with what it probes.

For many bugs this does not matter. For bugs that depend on precise timing, memory layout, or hardware interactions, it can matter enormously. The heisenbug — the bug that vanishes under observation — is real, and it is real precisely because observation is not free.


What the Debugger Reveals About the Code

There is a kind of information the debugger provides that is not about the bug at all. It is about the code.

When you step through an unfamiliar codebase with a debugger, the experience often tells you something the documentation did not. You see the actual call sequence, not the intended one. You see what objects actually contain at runtime, not what the type signature says they should contain. You see the path through conditionals that is actually taken.

This is the debugger as a reading tool, not a bug-finding tool. It is one of the better ways to understand code that is poorly documented or whose documentation has drifted from the implementation. The running system is the authoritative record of what the code does. The debugger gives you access to that record.

This is also where the gap between static and dynamic understanding becomes clear. Code that reads simply may behave complexly. Indirection, inheritance, dynamic dispatch, closures capturing mutable state — these things are hard to trace mentally through source code alone. Watching them execute can resolve confusion that reading the code did not.


The Probe and the Map

A useful way to think about the debugger is as a probe for building a map, not as a map itself.

You have a theory about how the system works. The theory is incomplete or has an error somewhere. You use the debugger to test specific hypotheses — "I think this value is X at this point" — and the probe confirms or refutes each one.

This is different from "running the program with the debugger attached and seeing what happens." That approach works, but it is slower and produces less insight. When you already have a hypothesis, the debugger resolves it efficiently. When you do not, you are searching blindly through state space.

The quality of your debugging is largely the quality of your hypotheses. A good debugger does not just know how to use the tool; they know how to form specific, testable predictions about program behavior. The tool is only as useful as the model driving it.


What This Means

Debuggers are good at showing you state. They are not good at explaining causality, and they do not try to. The gap between a wrong value and the reason for it is a gap you fill with reasoning, not with tools.

This means the limiting factor in debugging is rarely tool proficiency. It is almost always mental model accuracy. The engineer who understands the system well enough to form precise hypotheses will debug faster with a print statement than an engineer without that understanding will with a full-featured debugger.

The debugger amplifies your understanding. It does not substitute for it.