Debugging Like a Pro: Systematic Strategies That Save Hours
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?
- Add a log at line 50. Is the data correct there?
- Yes → Bug is in lines 51-100
- No → Bug is in lines 1-50
- 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.
- "The API always returns JSON." (Does it? What about 500 errors?)
- "This variable is never null." (Are you sure?)
- "This function only gets called once." (Check the logs.)
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
- Reproduce: Can you make it fail consistently?
- Isolate: Binary search to find the area
- Hypothesize: What do you think is happening?
- Test: Add a log or assertion to confirm
- Fix: Make the smallest change that solves it
- 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.