The Debugging Mindset
Reproduce, isolate, hypothesize. Read tracebacks bottom-up.
The best debuggers in the world aren't the ones who know the most tools. They're the ones with a method. Bugs feel random to beginners and inevitable to seniors, because the senior has internalized a process for hunting them. This lesson is that process.
Read the stack trace bottom-up
Stack traces look like spam. They're not. They're a recipe showing exactly how your program ended up at the error. Read them like a book: start at the bottom.
TypeError: Cannot read properties of undefined (reading 'name')
at formatUser (src/format.js:12:18)
at getProfile (src/profile.js:34:5)
at handler (src/api/route.js:21:10)
at processRequest (src/server.js:88:7)
at /node_modules/next/dist/server/index.js:201:14The bottom is "Node booting your request": code you didn't write and can't change. Skim up until you hit your code. That's where the call started: processRequest called handler called getProfile called formatUser. The top is where it died: formatUser at line 12, column 18.
Now you know two things: where the error happened (top frame) and how you got there(the frames below it). Often the actual bug is one frame up, where someone passed a bad argument that the top frame couldn't handle.
Distinguish symptoms from causes
The first thing you see is rarely the bug. It's a symptom. The button does nothing. The page is blank. The number is wrong. Those are all symptoms.
Symptom: the user sees their cart total as $NaN.
Possible cause: a price is undefined because a SKU was deleted.
Possible cause: a quantity is a string and arithmetic produced NaN.
Possible cause: tax calculation divided by zero.
Don't patch the symptom. Patching NaN to 0 hides the bug and creates new ones. Trace upstream until you find the actual cause, then fix it. The litmus test: ask "why does that happen?" five times. If you can't, you haven't found the root cause.
{ sku: null }. Why? We're calling price?.valuebut didn't check the whole product. Why? The fallback logic was added by someone else without context. Why?Because the type system says price is optional but the UI assumed it isn't. Now you're at the real fix.The reproduce / isolate / hypothesize loop
- Reproduce.Find the smallest input that triggers the bug, every time. If you can't reproduce, you can't fix it.
- Isolate.Strip away everything that doesn't matter. Comment out unrelated code. Run the failing case in a minimal sandbox. Each thing you remove either still triggers the bug (good, it wasn't the cause) or stops triggering it (great, that part matters).
- Hypothesize.Form a specific theory. "I think the date parser breaks on UTC midnight." Predict what you'd see if it's true. Then test.
- Verify or refute. Run an experiment. Got the result you predicted? Theory holds, fix it. Different result? Update the theory and go again.
Most beginners skip step 1 and 2 and jump straight to flailing. It's why a 4-hour bug is often a 4-minute bug once you finally sit down and reproduce it cleanly.
Binary search: cut the problem in half
This is the most reliable trick in debugging. If you don't know which line/change/commit caused a bug, you don't need to look at every line. Cut the suspect range in half, test, then keep cutting. Each split halves the remaining space. 1000 lines → 10 splits.
In code: comment things out
The page is broken and nobody knows why. Comment out the bottom half of the component. Does it still break? Bug is in the top half. Doesn't break? Bug is in the bottom half. Recurse.
In history: git bisect
The page works at version 1, broken at version 100. Which commit broke it?
git bisect start
git bisect bad # current state is broken
git bisect good v1.0 # this version worked
# git checks out the middle commit
# ... test the app ...
git bisect good # or 'git bisect bad'
# git picks the next middle
# ... keep testing ...
# eventually:
# b4f8a91 is the first bad commit
# commit b4f8a91
# Bump dependency X
git bisect reset # back to where you wereWith 100 commits you'll find the culprit in 7 steps. With 10,000 commits you'd need 14. Bisect is a superpower.
git bisect run ./test.sh walks the bisect for you in seconds.Rubber-duck debugging
Sit down and explain the problem, line by line, out loud, to a rubber duck. (A coworker, the kitchen, your cat. The duck is whoever's free.)
The phenomenon: the act of forming words to explain it forces your brain to actually re-examine its assumptions.Halfway through saying "and then the function returns the user's ID..." you'll catch yourself: "wait, does it? I never checked." That's the bug.
This is why explaining a bug to a colleague often produces the answer before they say a word. They were the rubber duck.
Useful console methods you'll forget exist
console.log for everything is fine. These are sharper tools:
// label values so you don't lose track of which is which
console.log({ user, count, items });
// {user: {…}, count: 4, items: Array(3)}
// pretty-print tabular data
console.table(users);
console.table(users, ["name", "role"]); // pick columns
// group related logs (collapsible)
console.group("Loading dashboard");
console.log("step 1: fetch user");
console.log("step 2: fetch projects");
console.groupEnd();
// measure how long things take
console.time("query");
await fetchSlow();
console.timeEnd("query");
// query: 423.21ms
// count how often a code path runs (great for finding accidental loops)
function onClick() {
console.count("button click");
}
// button click: 1
// button click: 2
// trace shows the stack from this point
function doThing() {
console.trace("got here");
}
// only log when an assertion fails
console.assert(arr.length, "Array should not be empty");Logging strategies that scale beyond one bug
- Log inputs, outputs, and decisions. Not internal state of every line. You want to be able to skim the trace and reconstruct the conversation.
- Log identifiers, not whole objects.
user.idis greppable.{...user}isn't. - Tag your logs. A consistent prefix like
[payments]lets you grep the noise away later. - Take logs out before merging. If they have long-term value, replace them with proper instrumentation (Sentry, OpenTelemetry, structured logs).
When you're truly stuck
- Write down what you've tried.A bullet list forces you to notice gaps in what you've actually verified.
- Step away. 15 minutes. Make tea. The number of bugs solved in the shower is non-trivial.
- Suspect the obvious.Is the dev server actually running? Are you looking at the right tab? Did your build succeed? The number of senior engineers who've spent an hour debugging a stale tab is non-zero.
- Read the docs and the source.Library bugs happen, but they're much rarer than "I'm using it wrong."
- Ask for help. A clear write-up of the problem (repro steps, expected, actual, what you tried) often solves itself as you write it.
Quick quiz
Where in a stack trace is the line that actually threw the error?
Recap
- Stack traces: top is where it died, bottom is how you got there.
- Symptoms ≠ causes. Ask "why" five times to reach the real fix.
- Reproduce → isolate → hypothesize → verify. Repeat until clarity.
- Binary search on code (comment halves) and on history (
git bisect). - Rubber-duck debugging works because it's actually you debugging, with the duck as an excuse.
console.table,group,time,count,trace,assertare sharper thanlog.