Lesson 1 of 6
Debugging & Troubleshooting
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are — by definition — not smart enough to debug it." — Brian Kernighan
You will spend more of your career debugging than writing new code — and nobody teaches it. Bootcamps teach you to build; the working world teaches you, painfully, that most of the job is figuring out why the thing you built is doing something you didn't ask for. This is the first chapter of Part X because debugging is the exact line between someone who can code and someone teams trust. The junior guesses and prays. The engineer runs an investigation.
Something is broken, you have no idea why, and staring at the code isn't helping. What does a professional actually *do* — step by step — to go from "it's broken" to "fixed and it won't come back"?
Core Concepts
Debugging is the scientific method applied to code. That single reframe is the whole chapter, so let it land. A scientist doesn't stare at a failing experiment and feel their way to the truth; they observe, form a hypothesis, run one controlled test, observe the result, and repeat until reality gives up its secret. Debugging is identical: observe the symptom → form a hypothesis about the cause → make ONE change to test it → observe → repeat. Everything else in this chapter is a technique in service of that loop. When you see a senior engineer fix in ten minutes a bug you fought for two hours, they were not smarter — they were running this loop tightly while you were guessing.
The mindset: the computer is not lying to you
The first thing that has to change is not your technique — it's your belief. The bug feels like magic, like the machine has developed a grudge. It hasn't. The computer is deterministic; it did exactly what it was told. The gap is not in reality — it's in your mental model of reality. Somewhere, what you believe the code does and what it actually does have quietly diverged, and your only job is to find that one spot. This is oddly freeing: it means the answer is always there, findable, never mystical. You are not chasing a ghost. You are looking for the single point where your assumptions stopped being true.
Reproduce first — always
Here is the rule that separates people who fix bugs from people who hope bugs go away: a bug you cannot reproduce, you cannot fix. If you can't make it happen on demand, you can't confirm you've cured it — a "fix" for an intermittent bug is just a coincidence waiting to be disproven in production. So the first real work is building a reliable repro: the exact input, state, and steps that make it fail every single time. Then you minimize it — strip away everything that isn't essential until you have the smallest possible case that still breaks. Minimization isn't tidiness; it's diagnosis. Half the time, the act of removing things until it stops failing points straight at the cause.
Read the error — actually read it
Beginners see a red wall of text and recoil. Engineers read it, because the traceback usually tells you both what went wrong and where. How you read a stack trace depends on the language: Python prints the actual exception at the bottom ("most recent call last"), while Java, JavaScript, Go, and C# put it at the top with the call chain below. Either way, find the frame where your code raised it — that line and its error type are the start of the trail. The error type (KeyError, NullPointerException, IndexError) is a category label that already narrows the hypothesis space enormously. Skipping the error message to go re-read your code is like ignoring the smoke alarm to go sniff around the house. Read the message. Read the line number. Then form your hypothesis.
Bisection: the master technique
When the error doesn't hand you the location, you hunt for it — and the professional's hunt is not linear, it's binary search over the problem space. You may recall binary search from the Foundations volume: halve the search space each step, O(log n) instead of O(n). The same move localizes a bug frighteningly fast. Suspect a 500-line function? Add one checkpoint in the middle: is the value already wrong here, or still correct? That one question eliminates half the code. Repeat. A bug hiding anywhere in a thousand lines falls out in about ten checks, not a thousand.
The technique scales beyond one function:
| Where the bug hides | Bisection move | You halve… |
|---|---|---|
| Inside one function | A print/breakpoint in the middle | The lines of code |
| Somewhere in your data | Test the first half of the input, then the other | The dataset |
| Introduced by some commit | git bisect between a known-good and known-bad commit |
The project's history |
| In a big config/HTML/CSS file | Comment out half, see if the symptom survives | The file |
git bisect deserves a special mention: point it at a commit that worked and one that's broken, and Git checks out the midpoint for you to test, again and again, converging on the exact commit that introduced the bug — even across a thousand commits, in about ten tests. Nothing localizes a regression faster.
The right tool for the moment
Techniques are only as good as knowing when to reach for each:
| Tool | Use it when… | Cost / caveat |
|---|---|---|
print / logging |
Quick, one-off, or in environments you can't attach to (production, CI) | Noisy; remember to remove or downgrade to debug level |
| A real debugger (breakpoints, stepping) | You need to inspect live state, step line by line, watch variables change | Setup cost; can't always attach to prod |
| Assertions | You want to catch a broken invariant the moment it breaks, not later | Only checks what you thought to assert |
| Rubber-duck debugging | You're stuck and looping — explain it out loud, line by line, to anyone (or a duck) | Feels silly; works embarrassingly often |
Print debugging is not shameful — it's the most-used debugger on Earth, and the only one that works in a production log. But a real debugger lets you pause execution and inspect every variable at the moment of failure, which beats sprinkling prints when the state is complex. And rubber-ducking — forcing yourself to explain the code aloud, one line at a time, to a rubber duck on your desk — works because articulating your assumptions out loud is exactly where you hear the one that's wrong. The flaw was invisible in your head and obvious in your mouth.
The cardinal rule: change one thing at a time
When you're frustrated, the temptation is shotgun debugging — change five things at once, re-run, and pray. Never do this. If it works, you don't know which change fixed it (so you can't be sure it's really fixed), and you've probably introduced two new bugs you'll meet next week. If it fails, you've learned nothing. Change one variable, observe, revert if it didn't help. This is the discipline that makes debugging converge instead of thrash. It is slower per step and far faster to the answer.
Know the usual suspects
Bugs cluster into a handful of families, and recognizing the family narrows the hunt:
- Off-by-one —
<vs<=,range(n)vsrange(n+1), the fencepost. The most common bug in programming. - Null / undefined — you assumed a value was there; it wasn't. The billion-dollar mistake.
- State & mutation — something changed a shared value out from under you. Especially vicious with mutable default arguments and shared objects.
- Async & races — two things ran in an order you didn't expect (recall threads and concurrency): the bug appears one run in fifty.
- Environment & config — "works on my machine." Different versions, missing env var, different OS. This is precisely the class Docker exists to kill by shipping the environment with the code.
- The error is not where it surfaces. The crash on line 200 was caused by a bad value set on line 40. The symptom and the cause are often far apart — which is why you trace backwards from the symptom, not forwards from the crash.
The runnable investigation
Here is the entire method in miniature. A function has a subtle off-by-one bug. We don't stare at it — we write a test that pins it, watch the assertion catch it, form a hypothesis, make one change, and watch the same test pass. This is debugging in fourteen lines:
Click Run to execute the code.
Notice what the assertion did. It turned a vague "the average looks wrong" into a precise, repeatable statement: expected 20.0, got 10.0. That message localized the bug to arithmetic, the small input made the wrong number obvious by hand, and the same assertion that caught the bug now proves the fix. We never guessed. We observed, hypothesized, changed exactly one thing, and observed again.
A string of Christmas lights goes dark and you have 100 bulbs to check. The beginner tests bulb 1, then 2, then 3 — O(n), up to 100 checks. The engineer unplugs the middle and checks which half is dead, then halves the dead half, and again — O(log n), about 7 checks for the whole string. Bisection isn't a programming trick; it's just the fastest way any human has ever found something in a big pile. Your code is the string of lights.
On 9 September 1947, operators of the Harvard Mark II relay computer traced a malfunction to an actual moth caught in Relay #70. They taped the moth into the logbook with the note: "First actual case of bug being found." The word "bug" for a technical fault predates this — Edison used it in the 1870s — which is exactly why they wrote "first actual bug," delighting in the literal insect. The logbook page, moth still attached, sits in the Smithsonian. The operator who kept it was working alongside Grace Hopper, who told the story for decades and helped make "debugging" the universal word for what you'll do most days of your career. A moth in a relay in 1947 named the single most common activity in software.
What experienced engineers actually do when something breaks:
• They reproduce before they theorize. The first question is never "why?" — it's "can I make it happen again, reliably, with the smallest input?"
• They read the whole traceback, bottom-up, out loud if needed. Nine times in ten the answer is already printed on the screen they were about to scroll past.
• They change one thing and write down what they expect *before* running. Being wrong about the prediction is the most useful signal there is — it means the broken assumption just revealed itself.
Certificate PDFs started crashing on generation — but only for some students, which is the "can't reproduce" trap. The fix was pure method: collect the failing cases, minimize, and the pattern appeared — every crashing student had a special character (an accent, an apostrophe) in their name. The traceback pointed at the PDF font layer, not the name-handling code, a textbook case of the error surfacing far from its cause. One reliable repro (`generate("Zoë O'Brien")`), one hypothesis (the font can't encode those glyphs), one change (Unicode-safe encoding), and — the part that mattered — a regression test with that exact name committed before the fix, so the crash can never quietly return.
Common Mistakes
- Guessing instead of investigating. Changing code because it "might be it," without a hypothesis you can confirm or reject. If you can't say what you expect to happen, you're not debugging — you're gambling.
- Not reading the error. Scrolling past the traceback to re-read your code, when the traceback names the exception, the file, and the line. Read it first, every time.
- Shotgun debugging. Changing several things at once. Now you can't attribute the outcome to any of them, and you've likely planted new bugs.
- Fixing the symptom, not the cause. Wrapping the crash in a
try/exceptthat swallows it, or patching the line that threw, when the bad value was born three functions earlier. The bug moves; it doesn't die. - Not reproducing first. "It seems fixed now" after a change you can't tie to the bug. Intermittent bugs cured by coincidence come back at the worst time.
- Fixing without a regression test. You solved it today and left the door open for it to return in six months, when nobody remembers this chapter.
A bug could be anywhere in a 1,024-line file, and you have no traceback pointing at it. Checking line by line from the top is up to 1,024 checks. Now suppose each check — a single well-placed print asking "is the value already wrong here?" — lets you rule out half the remaining lines. How many checks to corner it? Ten. (2¹⁰ = 1,024.) The same log₂ that made binary search fast in the Foundations volume is why a calm engineer with one print statement beats a panicked one with fifty. Bisection turns a haystack into ten questions.
Interview Corner
git bisect between the last known-good commit and HEAD; Git checks out the midpoint, I run the test, mark good or bad, and in about eight or nine steps it names the exact commit. From there the diff is small enough to reason about directly. Then the durable part: I add that failing test to the suite before shipping the fix, so CI catches this class forever, and I'd run a blameless post-mortem — the fact that a regression reached prod for 300 commits is a gap in our tests or CI, not in a person."- In the editor above, break
average_fixeda new way — changelen(nums)tolen(nums) - 1. Predict the wrong output before running, then confirm the assertion catches it. You just practised reproduce-then-pin. - Take any function longer than 30 lines you've written. Without reading it top-to-bottom, place a single print at its midpoint that checks whether the key value is already correct there. Notice how one print eliminated half the code from suspicion.
- Out loud (as in a micro1 screen), rubber-duck a recent bug you fixed: explain the code line by line to an imaginary duck until you reach the flaw. Note the exact sentence where the wrong assumption became audible.
Chapter Summary
- Debugging is the scientific method: observe → hypothesize → change one thing → observe → repeat. It's an investigation, not a guess.
- The mindset shift: the computer isn't lying and the bug isn't magic — your mental model diverged from reality somewhere, and you're finding where.
- Reproduce first: a bug you can't reliably reproduce, you can't fix. Then minimize the repro.
- Read the error. The traceback usually names what and where — read it carefully for the cause (in Python the exception sits at the bottom; in Java/JS at the top).
- Bisection is the master technique: halve the search space each step — a print in the middle,
git bisectacross commits —O(log n)to localize. - Change one thing at a time; shotgun debugging hides the real cause. Most debugging time is spent locating, not fixing.
- The real cure for a bug is a regression test that reproduces it, added before the fix — and a blameless culture so bugs get surfaced, not buried.
Debugging assumes you can find your way around the code in the first place — but the hardest bugs live in code you didn't write, in a codebase far too large to hold in your head. That's the reality of every real engineering job: you join, and there are already 200,000 lines waiting. The next chapter is the skill nobody lists on a résumé but every senior engineer has quietly mastered — Reading Large Codebases: how to navigate, trace, and build a mental map of a system you didn't create.
Editorial reflection — juniors think debugging is a sign of failure — proof they wrote bad code. It's the opposite. Bugs are inevitable; the difference between engineers is not who avoids them but who hunts them methodically. The calm, curious person running a tight observe-hypothesize-test loop while everyone else panics is the one the team learns to trust in a crisis. Master this, and you become the person they call when the site is down at 3am — which is another way of saying, indispensable.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.