It’s 11pm. Your code was working fine an hour ago. Now it isn’t. You’ve stared at the same 20 lines for so long they’ve lost all meaning, and your git history is a graveyard of changes that “should have fixed it.”
Sound familiar? Debugging is the part of programming nobody really teaches you — you just absorb it, painfully, over time. But there are smarter ways to go about it. Here’s what actually works when you’re completely lost.
Stop Touching Things — Understand the Problem First
Before you change a single line of code, stop. Reproduce the bug deliberately. Can you make it happen again? Every time? Only sometimes? Under specific conditions?
This matters more than you think. An intermittent bug is a completely different animal from a consistent one. Write down the exact steps that trigger the error — the input, the expected output, what you’re actually getting instead.
Here’s a useful test: can you describe the bug in one clear sentence? If you can’t, you don’t understand it well enough to fix it yet. That sentence becomes your north star for the entire session — everything you try should be aimed at proving or disproving it.
The Print Statement: Embarrassingly Simple, Annoyingly Effective
Yes, really. Before you reach for a fancy debugger, just print stuff.
print(f"Before calculateTotal() — items: {items}")
result = calculateTotal(items)
print(f"After calculateTotal() — result: {result}")
It sounds too basic to be useful. It isn’t. A well-placed print statement reveals whether your assumptions about the data are actually true. Nine times out of ten, the bug isn’t where you think it is — it’s three functions upstream, where the data went wrong long before you saw the crash.
Work backwards from the error. Find the last point where everything looked correct. The bug lives somewhere between there and the failure. Print statements light the path.
Real Debuggers: You’re Probably Not Using Them Right
Once you’ve got a theory about where the bug lives, bring in the real tools. Chrome DevTools for JavaScript, pdb for Python, the built-in debugger in VS Code or JetBrains. These let you pause execution mid-run and look around — inspect every variable, step line-by-line, and see exactly what’s happening in the moment.
The feature most developers ignore: conditional breakpoints. Instead of stopping on every single loop iteration, you tell the debugger to pause only when i === 47 or user.role === null. If your bug only surfaces after 300 iterations, this alone saves you an hour of clicking “next.”
If you haven’t taken 20 minutes to properly learn your debugger’s UI, that’s the highest-ROI thing you can do this week.
Stack Traces Are Telling You Exactly What Happened — Learn to Read Them
When your code crashes, don’t panic at the wall of red text. That’s not noise — it’s a map. Read it from the bottom up. The bottom line is where the error originated. Every line above it is a function call that led to that moment.
TypeError: Cannot read properties of undefined (reading 'name')
at formatUser (utils.js:42)
at renderProfile (profile.js:18)
at App.render (App.jsx:73)
Here, utils.js line 42 is trying to read .name on something that is undefined. The real question isn’t “why does line 42 crash?” — it’s “why is formatUser() receiving undefined in the first place?” Go one level up and ask that. Stack traces don’t just show you what broke — they show you the full sequence of events that caused it.
One Change at a Time. This Rule Has No Exceptions.
When you think you’ve found the issue, the temptation is overwhelming: fix five things at once and see if it works. Don’t.
If you make multiple changes and the bug disappears, you have no idea which one actually fixed it. If things get worse, you can’t tell what you broke further. You’re now debugging the debugging.
Make one change. Run it. See what happens. Then decide the next move. This feels painfully slow — it’s actually the fastest path to a confident fix, because you always know exactly what caused each result.
When You’ve Been Staring Too Long, Stop Staring
There’s a reason rubber duck debugging is a real, legitimate technique. Explaining your problem out loud — even to an inanimate object — forces you to articulate assumptions you’d been silently making. You’ll hear yourself say “so I’m assuming this function always returns an array…” and suddenly realize: does it, though?
When the duck isn’t enough, find a real human. Stack Overflow, a Slack group, a teammate. The key: describe what you’ve tried, what you expected, and what you’re seeing. Be specific. You’ll get better answers faster — and surprisingly often, you’ll spot the bug yourself while writing the question out.
The Short Version
- Reproduce it first — understand the bug before you touch anything
- Print statements are your fastest way to test assumptions about your data
- Learn your debugger properly — conditional breakpoints will save you hours
- Stack traces: read bottom-up — they’re maps, not noise
- One change at a time, always, no exceptions
- Talk it out — rubber ducking works, and so does asking for help
Sources
- McConnell, S. (2004). Code Complete. Microsoft Press.
- Hunt, A., & Thomas, D. (1999). The Pragmatic Programmer. Addison-Wesley.
- Pike, R. (2011). Debugging Techniques: The Art of Debugging. O’Reilly Media.