Open a bug report and there's a good chance the first reply is a screenshot of an error message with the words "have you read this?" underneath. That reply annoys people. It also happens to be right about 70% of the time.
I've watched this play out on teams I've worked with, and in my own code more times than I'd like to admit. The mistakes that sink new developers while debugging aren't exotic. They're boring, predictable, and almost entirely avoidable. What follows is the version of this list I wish someone had handed me when I started: not "read your errors" as a slogan, but what that actually looks like in practice, why your brain fights against it, and how to build a debugging habit that survives contact with a real codebase.
Key Takeaways
- Read the error message before you touch a single line of code — the answer is usually sitting in the stack trace.
- Reproduce the bug reliably before you try to fix it. An intermittent bug you can't reproduce is a bug you can't verify you've fixed.
- Change one thing at a time. Shotgun debugging feels productive and destroys the evidence you need.
- Know which of the three error types you're dealing with: syntax, runtime, or logic. Each demands a different approach.
- Use the tools that exist — debuggers, breakpoints,
git bisect, watch expressions. Print statements are a fine start, not the finish line. - A bug you can't explain after fixing is a bug that will come back. Understand the cause, not just the symptom.
Common debugging mistakes new developers make (and how to stop making them)
Here's the pattern I see most often. A developer hits an error, immediately starts editing code in the general vicinity of where they think the problem is, runs the program again, and repeats. Twenty minutes later they've changed six things, the error is different, and they have no idea which change caused which result. This is called shotgun debugging, and it's the single most expensive habit in early-career development.
Before the list, I want to be clear about one thing: almost every mistake below comes from skipping a step because you're panicking. Panic is the real enemy. The techniques are just ways to interrupt it.
Mistake 1: not reading the error message
The error message is not noise. It's the compiler or interpreter telling you exactly what it didn't like and roughly where. When a developer says "the error makes no sense," what they usually mean is "I read the first line and gave up."
Read it slowly, from the top. For a stack trace, read from the bottom up — the last frame that belongs to your code is almost always the real starting point, not the framework internals above it. I've lost count of the number of times the honest answer to "why is this crashing?" was printed cleanly in the terminal all along.
And if you don't recognize the error text, paste it into a search engine verbatim, minus the parts specific to your code (file paths, variable names). Someone has hit it before. The nuance is that you need to understand the cause described in the answer, not just copy the fix — otherwise you'll be back here tomorrow.
Mistake 2: guessing instead of reproducing
You cannot fix a bug you cannot reproduce. This is the sentence I'd tattoo on every junior developer's monitor if I could.
When a bug appears, your first job is not to fix it. It's to produce the smallest, most reliable set of steps that make it happen every single time. Same input, same state, same result. Until you can do that, every "fix" you try is a guess, and you have no way to know if it worked.
Reproduction also shrinks the problem. If a bug happens when you click a specific button with a specific record loaded, you've already eliminated 95% of your codebase from suspicion. That's a massive gift.
Mistake 3: fixing the symptom, not the cause
Say a value comes back as undefined and crashes your function. You add a check: if it's undefined, substitute a default and carry on. The crash is gone. The bug is not.
Why was it undefined? Was it a failed request? A race condition? A field that changed shape in an API response? If you don't answer that question, you've hidden the symptom and left the cause running free in your code. It will surface again somewhere worse — probably in production, probably at 2am.
The discipline is simple to state and hard to practice: before you patch, explain. If you can't articulate in one sentence why the bug happens, you don't understand it yet, and your fix is a coincidence.
The three types of errors in programming, and why the distinction matters
New developers treat all bugs as one category. They aren't. There are three types of errors in programming, and each one fails in a way that tells you where to look.
| Error type | When it appears | What it looks like | Where to look first |
|---|---|---|---|
| Syntax error | Before the program runs | The code won't compile or parse | Exact line and column in the message |
| Runtime error | While the program runs | Crashes, exceptions, stack traces | Last frame of the stack trace in your code |
| Logic error | Silently, after running fine | Wrong output, no crash, no message | Your assumptions about data and control flow |
The trap is treating a logic error like a runtime error. If your program runs without complaint but produces the wrong number, there is no error message to read, no stack trace to follow. You're now in the hardest territory: the bug is in your thinking, not your syntax. Debugging a logic error means proving your assumptions about what the code is doing at each step, one by one — which is exactly why breakpoints and watch expressions exist.
Mistakes that show up once your code actually runs
Changing more than one thing at a time
Shotgun debugging deserves its own headline. The rule is brutal and absolute: change one thing, then test. If you change two things and the bug disappears, you don't know which change fixed it, and you don't know whether the other change introduced a new bug that will surface later.
This feels slow. It isn't. One careful change per test cycle beats twenty random changes every time.
Ignoring the tools sitting right in front of you
Print statements are a legitimate debugging tool. They are also the only one many new developers ever learn. Meanwhile a debugger lets you pause execution, inspect every variable in scope, step line by line, and set conditional breakpoints that only trigger when a value is wrong. For a bug that happens once in fifty runs, a conditional breakpoint saves you an afternoon.
Two more you should know about:
git bisect— when you know a feature used to work and now doesn't, this walks your commit history and pinpoints the exact commit that broke it. It's binary search applied to your repo, and it's genuinely magic the first time you use it.- Rubber duck debugging — explain the problem out loud, line by line, to anyone or anything. The act of verbalizing forces you to notice the assumption you skipped. I've solved bugs mid-sentence more times than I can count, before I even finished explaining the problem.
Debugging in production
Never test a fix by deploying it and watching what happens. Reproduce locally or in a staging environment, fix it there, verify it there, then deploy. Debugging live is how a small bug becomes an outage, and it teaches you nothing because you can't inspect the state you're trying to understand.
Questions developers ask when they're stuck
Why does my code work and then randomly stop working?
If it stops randomly, it isn't random — it's timing-dependent. Two of the usual suspects are race conditions (two things running at once and interfering) and uninitialized state (a value that's empty the first time and leftover garbage the next). The fix is to make the bug deterministic: add logging, freeze the timing, run it in a loop until it fails. Reproducing it reliably is more than half the battle, and for intermittent bugs it's often the whole thing.
I copied this fix from a forum and it still doesn't work. Why?
Because the answer was written for someone else's code. Two developers can hit the same error message from completely different causes. Copying a fix without understanding the cause is a coin flip. Read the explanation, adapt the reasoning to your situation, and if you can't explain why the fix works, don't ship it.
Do I really need to learn a debugger?
Yes, and the sooner the better. Print statements tell you what a value was. A debugger tells you everything happening at the exact moment a line runs — the whole state, not the one variable you thought to print. The upfront cost is an afternoon of learning your editor's debugger. The payoff is years of faster bug hunting.
Turning this into a habit
None of these techniques is complicated in isolation. The hard part is doing them when you're frustrated and the obvious wrong move — start editing, start guessing — feels faster.
So here's the loop I'd actually recommend, in order, every time a bug shows up:
- Read the error message, top to bottom. Don't touch the code yet.
- Reproduce it reliably. Same steps, same result, every time.
- Say out loud, in one sentence, what you think is happening. If you can't, you've found your next task.
- Change one thing, then test. One thing.
- Fix the cause, not the symptom. If you're patching a symptom because the cause is too hard, say so honestly and file a ticket — don't pretend it's fixed.
Run that loop enough times and something shifts. You stop dreading bugs. Not because they get easier, but because the process stops being a mystery and becomes a method. The code is broken, and you have a way to find out why. That's the whole job.
So the next time an error message pops up and your hand reaches for the keyboard, try something radical: close the editor for thirty seconds and just read the message. It's not going to fix the bug for you. But it will tell you where to look — and on a bad day, that's most of the way there.