Debugging Mindset — The Hypothesis-Verification Cycle
Debugging Mindset — The Hypothesis-Verification Cycle
Reports that the time spent fixing already-built code is longer than the time spent building new features are common. Debugging is closer to a mindset than a separate skill. We honestly examine what we're assuming and what we haven't verified. This article covers the general flow of debugging, the hypothesis-verification cycle, the place of print and the debugger, bisecting and rubber-duck, and reading stack traces.
1. About debugging
The word "debug" itself is often traced to Grace Hopper's 1947 anecdote. The story goes that a moth was caught between the relays of the Harvard Mark II computer, removed, and noted in the log as "first actual case of bug being found." The expression "bug" appeared earlier — even in 19th-century letters of Thomas Edison — but this anecdote is often cited as the moment it stuck in the computer industry.
Classic debugging texts:
- Brian Kernighan & Rob Pike, The Practice of Programming (1999) — chapter 5 on debugging.
- David J. Agans, Debugging (2002) — nine rules.
- Andy Hunt & Dave Thomas, The Pragmatic Programmer (1999) — debugging chapter, the source of rubber-ducking.
2. Hypothesis → verification cycle
The skeleton of systematic debugging:
- Observe the phenomenon — what happened. Exactly.
- Form a hypothesis — "this seems to be the cause."
- Design a verification experiment — if the hypothesis is right, what experiment yields what result.
- Run it — the experiment.
- Compare results — was the hypothesis right or wrong.
- If right, fix it; if wrong, start over from step 1 with a new hypothesis.
The key is step 5. When the result shows the hypothesis is wrong, drop the hypothesis cleanly. People tend to bend results to fit their own hypothesis (confirmation bias). Many writers call debugging a fight against this bias.
3. print debugging vs the debugger
| Tool | Strength | Weakness |
|---|---|---|
console.log, print |
Anywhere, instantly. See flow as a timeline | Logs scatter and accumulate, cleanup burden |
| Debugger (breakpoints) | Rich variables and call stack, step-through | Async and multi-thread spots remain hard |
The two aren't in opposition. Once familiar, we naturally mix both. In the early stages, print alone takes us far enough.
Small tips for print:
console.log("[user load] before fetch", { userId });
console.log("[user load] response", response.status, await response.clone().text());
- Tag where it's printed.
- Print whole objects. Don't print only the one field we suspect.
- Remove logs after diagnosis. If they need to stay, move them to a real logger (winston, pino, logback).
4. Binary search (bisect)
Cutting a large problem in half repeatedly to narrow in:
Try 1: full code (200 lines) → error
Try 2: comment out first 100 lines → error gone ⇒ cause is in the first 100 lines
Try 3: keep only first 50 lines → no error ⇒ cause is in lines 50-100
Try 4: keep only first 75 lines → error ⇒ cause is in lines 50-75
... narrow to one line or one function in 8-10 tries
The same pattern works in git — git bisect:
git bisect start
git bisect bad # mark current commit as broken
git bisect good v1.4.0 # past tag that was working
# git moves to a middle commit
# verify behavior, then run git bisect good or git bisect bad
git bisect reset # return to where we started
Among tens or hundreds of commits, find the single commit that introduced the regression in log(2) tries.
5. Rubber-duck debugging
A technique of explaining our own code line by line to another person (or a desk toy). Documented in The Pragmatic Programmer (1999), and floating around as Unix folklore even before that.
While explaining, the spot where our assumption diverges from the actual code pops out of our own mouth. That spot is usually the bug.
In the LLM era, a chatbot makes a fine rubber duck. Don't expect the answer — the act of explaining alone is effective.
6. How to read a stack trace
The chain of function calls where the error fired:
TypeError: Cannot read properties of null (reading 'name')
at renderUser (src/views/user.tsx:42:18)
at processChild (node_modules/react-dom/.../react-dom.js:13987:14)
at processBlock (node_modules/react-dom/.../react-dom.js:14123:9)
...
Common spots to look:
- Error type —
TypeError,RangeError,ReferenceError,SyntaxError. Different meanings. - Message — the biggest clue.
- First line in our own code — suspect our own files before library internals.
- Line number — the exact location.
If source maps are alive, even minified JS shows the original location.
7. General-pitfall checklist
Worth running before going deep:
- Are we really running the same code? Is build cache or a service worker handing back an old version?
- Is it really the same environment? Did
.envnot get read? - Is the input data really what we assumed? Print it once.
- Is the async order what we assumed? Are we using a value before the Promise resolved?
- Are multiple places writing to the same resource at once?
8. Branches of debuggers
- Built into the language — Node has
--inspect, Python haspdb/pudb, Java has JDWP, Go has Delve. - IDE integrations — VS Code, IntelliJ support nearly every language.
- Browser DevTools — the debugger in the Sources tab.
- Time-travel debugging — rr (Linux), Replay.io. Run backwards too.
- Log-based — places where a debugger isn't usable, like production. Structured logging + tracing.
9. A small debugging flow
Symptom: form submit sometimes doesn't reach the server
1) Hypothesis 1: client validation is blocking
Experiment: console.log at the start of validation → always passes → drop hypothesis
2) Hypothesis 2: the network request never goes out
Experiment: DevTools Network tab → sometimes not visible at all → keep hypothesis
3) Hypothesis 3: the button is outside the form, so a double-click misses onSubmit on the second
Experiment: check HTML structure → button is a child of form, fine → drop hypothesis
4) Hypothesis 4: the first click's async work isn't done before the second click blocks onSubmit
Experiment: add double-click prevention disable → can't reproduce → keep hypothesis
→ adopt the fix
10. Common pitfalls
"It can't possibly be here" — the answer sits in the spot we didn't suspect. No exclusion without verification.
Changing several places at once — we don't know which one had the effect. One spot at a time.
Hanging on the same spot for a long time — when stuck for 30 minutes, get up. Walks, sleep, the next morning often produce the answer.
Ending debugging without leaving notes — if we hit the same spot again, we start from scratch.
Brushing an unreproducible bug off as "an environment thing" — only as a temporary measure when out of time. Build a reproduction environment when possible.
Not reading the error message — half of debugging time vanishes when we read the message calmly.
Closing thoughts
Debugging is not a separate skill but a mindset. The hypothesis → verification → result-comparison → new-hypothesis cycle is the core. Change one spot at a time, get up after 30 minutes stuck, read error messages calmly. With the five tools together — print, debugger, bisect, rubber duck, stack trace — we move fastest.
Next
- (end of learning)
The Practice of Programming (Kernighan & Pike, 1999) · Debugging (David J. Agans, 2002) · The Pragmatic Programmer · Chrome DevTools Sources panel · Python pdb docs · git bisect docs · The First Computer Bug · rubberduckdebugging.com · How to Report Bugs Effectively (Simon Tatham) for reference.