Early in my career, I debugged by changing random things and refreshing. Change a line, check if it works. No? Try another line. Paste the error into Google. This is "shotgun debugging"—spray and pray.

I watched a senior engineer debug a gnarly issue. She didn't touch the keyboard for 10 minutes. Just read code, drew a diagram, formed a hypothesis. Then made exactly one change. Fixed.

That's when I realized debugging is a skill you can learn, not just a talent you're born with.

First: Reproduce Reliably

You can't fix what you can't verify. If a user reports "it crashes sometimes," that's not enough. Find the exact steps: "Login, click Settings, click Profile within 2 seconds."

If you fix something without reproducing it first, you haven't fixed it—you've hidden it. You need to see it fail before you can prove it passes.

Binary Search Your Code

When you don't know where the bug is, don't read every line. Divide and conquer.

Got 100 lines of code producing wrong output?

  1. Add a log at line 50. Is the data correct there?
  2. Yes → Bug is in lines 51-100
  3. No → Bug is in lines 1-50
  4. Repeat. 100 lines → 50 → 25 → 12 → 6 → found it.
def complex_process(data):
    data = step_one(data)
    print(f"After step 1: {data}")  # Checkpoint
    
    data = step_two(data)
    print(f"After step 2: {data}")  # Checkpoint
    
    return step_three(data)

This also works with git. If v1.0 worked and v2.0 is broken, use git bisect. It automatically checks out the middle commit for you to test, rapidly finding exactly which commit introduced the bug.

Rubber Duck Debugging

It sounds silly. It works.

Explain your code out loud, line by line, to anything—a rubber duck, a plant, an imaginary coworker. "This loop iterates over users... then checks if active... wait."

The act of articulating engages a different part of your brain. You stop assuming what the code does and start verifying what it actually says. I solve about half my bugs just by trying to explain them to someone.

Question Your Assumptions

Bugs hide in the things you believe are true.

When stuck, list your assumptions. Then prove them wrong:

function processUser(user) {
    // Assumption: user exists
    if (!user) {
        console.error("WAIT - user is null here!");
        return;
    }
    // ...
}

Actually Read the Error

This sounds obvious, but I've watched developers stare at error messages without reading them.

TypeError: Cannot read property 'map' of undefined

This tells you exactly what's wrong. You're calling .map() on something that doesn't exist. Don't stare at the map function—trace back where that variable came from. The error message tells you the problem; the stack trace tells you where.

The Process

  1. Reproduce: Can you make it fail consistently?
  2. Isolate: Binary search to find the area
  3. Hypothesize: What do you think is happening?
  4. Test: Add a log or assertion to confirm
  5. Fix: Make the smallest change that solves it
  6. Verify: Does the original reproduction now pass?

Next time you hit a tough bug, resist the urge to change things randomly. Stop. Read. Think. Then act. You'll solve it faster than you would have by guessing.

← Back to Debugging & Code Quality

Back to Home