I don’t teach debugging as a separate lesson. I teach it constantly, because it’s the same skill under a dozen names: finding out where what you expected and what actually happened stopped agreeing, in code, in a proof, in a lab writeup, anywhere.
The debugging mindset
Most beginners debug by hoping. They change something that looks plausible, run the program again, and repeat until it works or they run out of patience. That works on a ten-line program. It stops working the moment the program is big enough that a random change can break something you weren’t even looking at, and you’ve lost track of what you touched.
The alternative is to treat it like an experiment. Form a hypothesis about what’s causing the bug. Design a test that would actually confirm or rule it out. Run the test. Update what you believe about the program based on what happened, and go again.
It’s the same loop that runs underneath any experimental science. The program is a system whose behavior you don’t fully understand yet, and debugging is just the process of building an accurate model of it.
Breaking problems down
When a program produces the wrong output, there’s one question that matters: where does the wrong value first show up?
Inside a single function, bisect it. Add a check partway through to cut the search space in half. Wrong by line 5, and the bug lives in lines 1 through 5. Correct by line 5, and it’s somewhere in what comes after. The insight that carries outside of code is that hard problems get easier by making them smaller — by eliminating what isn’t the cause, not by adding more guesses.
Distinguishing symptoms from causes
The common mistake is treating the symptom as the cause. The crash on line 47 looks like the bug, but it might just be where a null value from line 12 finally catches up with you. Asking “what happened upstream that led to this?” is the useful question, because it points you at causes instead of symptoms. The same habit, once it’s built, shows up as asking “why” more than once in math or anywhere else — not stopping at the first explanation that sounds plausible.
Using an IDE effectively
I only bring in the debugger tools once the mindset is in place: breakpoints, stepping through execution line by line, inspecting variables as they change. Handed to someone who hasn’t built the mindset yet, those tools just give them more ways to poke at the program randomly. Handed to someone who has, every click is deliberate. The tools have to follow the thinking, not replace it.
What to actually do in a session
When a student brings me a broken program, I don’t start by reading the code. I ask what they expect it to do, and what it’s actually doing. If they can point to exactly where those two answers split, they usually already know the bug: the gap between expectation and reality is the cause, most of the time. If they can’t point to it yet, that’s the actual task — not finding the bug, but finding where the two stories stop matching, which is the more useful skill and the one that survives outside of code.