Debugging and Reasoning About Programs
How to find bugs systematically — and how to write code you can reason about in the first place.
Every programmer spends more time reading and debugging code than writing it. The good news is that debugging is a skill — not a talent — and it gets dramatically easier once you have a process.
This chapter covers two things:
- Reasoning about code so you understand what it should do.
- Debugging code that isn't doing what you thought.
What debugging really is
A bug exists because there's a gap between:
- the mental model you have of what the program does, and
- what the program actually does when it runs.
Debugging is the process of finding where and why those two differ. You're not "fixing the code"; you're updating your model until it matches reality, then fixing the code.
If you find yourself thinking "this should work" — you've already discovered the bug. The gap between "should" and "does" is the entire problem.
A scientific-method approach
When something goes wrong, resist the urge to start changing code. Instead, follow a tiny loop:
The biggest beginner mistake is jumping straight to fix. The person who slows down at "form a hypothesis" finishes faster.
The most useful debugging tool: console.log
It's not glamorous, but console.log solves more bugs than any
debugger. The trick is to print what you assumed, not the
final answer.
If you log the types of the inputs and the running total, the bug screams at you:
The third iteration shows total becoming the string "64" —
that's the moment something went wrong. Now you know where.
Better than console.log: structured logging
A few small habits make logs much more useful:
- Label what you're printing:
console.log("user:", user)is far better thanconsole.log(user)when 10 logs appear at once. - Group related logs:
console.group("processing order", id)console.groupEnd().
- Use
console.tablefor arrays of objects:
Reading a stack trace
When something throws, JS prints a stack trace — a snapshot of the call stack at the moment of the error.
Read stack traces top to bottom:
- The message tells you what went wrong.
- The top frame tells you where it threw.
- The lower frames tell you how the program got there.
If a function appears in the trace but you didn't call it directly, the line above it called it. Walk up until you find your code (vs. library code) — that's usually the most informative line.
Common error patterns
A small zoo of errors you'll meet often:
A pattern: errors usually tell you exactly what went wrong if
you read them. "Cannot read property X of undefined" means
"something I expected to be an object was undefined — and I tried
to read X from it." Find what that "something" is.
Defensive thinking: invariants
An invariant is a fact about your program that should always be true at a given point. Stating them in your head — or in comments, assertions, or tests — gives you anchors.
When a bug appears, ask: which invariant did I think held that actually didn't? That question, asked out loud, often is the debugging session.
Rubber-duck debugging
The classic trick: explain the code, line by line, out loud, to an inanimate object — traditionally a rubber duck. The act of articulating "and then this returns... wait, no it doesn't" catches a huge fraction of bugs.
You don't need a duck. You just need to slow down enough to describe what you think is happening.
Binary search bug-hunting
When the bug is "somewhere in this big code path", don't read top to bottom — bisect.
- Pick a point in the middle.
- Print or check: is the state correct here?
- If yes, the bug is after this point. If no, it's before.
- Repeat in the smaller half.
This narrows down even huge codebases in a handful of steps.
Reproducing the bug
The single most powerful debugging technique: shrink the failing case to the smallest reliably-reproducing example.
- "My app crashes sometimes" — useless.
- "My app crashes when I open the user page" — better.
- "It crashes in
formatDatewhen a user has nobirthdayfield, but not otherwise" — that's a fix request, not a bug report.
When you can reproduce a bug on demand, you've already done 80% of the work.
Writing code that's easier to reason about
The best debugging is not needing to debug. A few habits that slash bug rates:
- Small functions. A 5-line function is easier to verify correct than a 50-line one.
- Pure when possible. As you saw in the FP chapter, side effects make code harder to reason about.
- Name things precisely.
dataandresultsay nothing.filteredOrdersandrevenueByMonthsay a lot. - Use
constby default. A variable that never changes can't secretly be the wrong value somewhere. - Guard at the edges. Validate inputs, then trust them inside.
- Don't be clever. Clear code beats clever code. The code you write today, your future self has to read tomorrow.
Multi-file: a bug-hunting walk-through
This module has a subtle bug. Run it, then read the code and find the bug before looking at the fix below.
Symptoms you'd notice:
- The
totalis correct on the first call. - But run
applyDiscounts(cart, 0.1)again and the total drops further every time — because each call mutatesitem.price.
The fix: don't mutate the caller's data.
function applyDiscounts(cart, discount) {
return cart.reduce(
(total, item) => total + item.price * (1 - discount) * item.qty,
0,
);
}This is a classic immutability bug — the same one we warned about in the functional intuition chapter.
Challenge
Below is a function average(nums) that is supposed to return the arithmetic mean of an array of numbers, ignoring any non-number entries (strings, nulls, etc.). It has at least one bug.
Your task:
- Identify the issue(s) by reasoning about the code.
- Replace the function with a correct implementation.
- It must return
0for an empty array AND for an array with no valid numbers (avoidNaNfrom dividing by zero). - It must not mutate the input array.
A function you wrote is returning the wrong number. You've stared at the code for ten minutes. What's the most effective next step?
Rewrite the function from scratch and hope the new version is correct
Search the web for "JavaScript function wrong number"
Add logs that print your inputs, intermediate values, and assumptions — then re-run to see where the actual values diverge from what you expected
Add try/catch around the call to suppress the error