HomeProjectsBlogContact

How to Debug Code Systematically: A Method That Works on Any Bug

Debugging is not about being clever. It is a repeatable process: reproduce, observe, narrow down, fix the cause, and prove it. The method experienced developers use on any bug.

How to Debug Code Systematically: A Method That Works on Any Bug

Watch a beginner and an experienced developer debug the same problem and you'll notice something. The experienced one isn't necessarily smarter. They're just less random. They follow a process, and that process works whether the bug is in a React component, a SQL query or a deployment script.

Here's that process.

Step 1: reproduce it reliably

You can't fix what you can't trigger. Before touching any code, find the exact steps that make the bug appear.

  • What input, user, browser or environment?
  • Does it happen every time, or sometimes?
  • Did it ever work? When did it stop?

Write the steps down. If the bug only appears "sometimes," that's a clue in itself: timing, caching, race conditions, or data that differs between users.

The ideal result is a minimal reproduction: the smallest piece of code or the shortest set of steps that still shows the problem. Building one often reveals the bug before you even finish.

Step 2: read what the computer is telling you

Error messages and stack traces are the most underused debugging tool there is. Read the whole message, find the first line in the stack trace that points to your code, and go there. If messages feel like noise, this guide helps: how to read error messages and stack traces.

Also check the places errors hide:

  • Browser console and the Network tab (status codes and response bodies)
  • Server logs
  • Database logs or slow query logs
  • Your hosting platform's function or build logs

Step 3: form a hypothesis

Don't change things at random. State a guess you can test:

"I think the user ID is undefined when the profile request is made."

A good hypothesis is specific and checkable. A bad one is "something's wrong with React."

Step 4: test the hypothesis by observing

Now gather evidence. Pick the cheapest tool that answers the question:

  • Logging. console.log, print, dd(). Log values and types, not just "got here." console.log({ userId, type: typeof userId }) beats console.log('here').
  • A debugger. Breakpoints in VS Code or browser DevTools let you pause execution and inspect everything in scope. Once you're used to them, you'll reach for them constantly.
  • Network inspection. Is the request sent? With which payload? What came back?
  • Direct queries. Run the SQL yourself. Call the API with curl. Remove layers until you see the raw truth.

If the evidence contradicts your hypothesis, good. You've learned something. Form the next one.

Step 5: narrow it down (binary search)

When you have no idea where the bug lives, cut the problem in half repeatedly.

  • Comment out half the code path. Still broken? The bug is in the other half.
  • Log at the midpoint of a data pipeline. Is the value correct there?
  • Disable half your browser extensions, middleware or plugins.

For "it used to work," Git has this built in:

git bisect start
git bisect bad              # current commit is broken
git bisect good v1.4.0      # this one worked
# Git checks out a commit in between; test it, then:
git bisect good   # or: git bisect bad

Git keeps halving until it identifies the exact commit that introduced the bug. On a long history, that's a handful of steps instead of hours of reading diffs.

Step 6: fix the cause, not the symptom

Say a value is sometimes null and crashes the page. The symptom fix is value?.name ?? ''. The real question is why is it null? Maybe a request races another. Maybe a database row is missing a foreign key. Maybe validation is absent.

Sometimes a defensive fix is the right call. But make it a decision, not a reflex. Symptom fixes tend to move the bug somewhere harder to find.

Step 7: prove it, and keep it fixed

  • Run your original reproduction steps. Is the bug gone?
  • Check nearby behaviour you might have affected.
  • Write a test that fails without the fix and passes with it. That bug can never quietly come back.

Habits that speed everything up

  • Explain the problem out loud (rubber duck debugging). Saying "this function receives the user and then..." often reveals the gap mid-sentence.
  • Check your assumptions first. Is the file you're editing the one that's running? Is the server restarted? Is the cache cleared? A huge share of "impossible" bugs are stale builds.
  • Take a break after 45 minutes stuck. Fresh eyes notice things tired ones skip.
  • Change one thing at a time. Change three things and it works? You don't know which one fixed it, or whether the other two broke something.
  • Keep a bug journal. A short note on each nasty bug and its cause. Patterns emerge quickly.

When to ask for help

After you've reproduced it, read the errors, and tested a couple of hypotheses, ask. Include what you expected, what happened, the minimal reproduction, and what you've already ruled out. That's a question people want to answer, and half the time you'll solve it while writing it.

AI assistants are useful here too, if you give them the same context and verify what they suggest. More on that in using AI coding assistants without getting worse at coding.

FAQ

What is the fastest way to debug code?

Reproduce the bug reliably, read the full error message, then narrow the location by halving the problem space. Random code changes feel fast but almost always take longer.

Should I use console.log or a debugger?

Both. Logging is quick and works anywhere, including production. A debugger is better for complex state, because you can inspect everything at a paused moment and step through execution.

How do I debug a bug I can't reproduce?

Add logging around the suspected area, capture context (user, input, timing, environment) in production with an error tracker, and look for patterns across occurrences until you can reproduce it.

Keep reading

More from the workbench

All articles →