Key Takeaways
- Read the full error message first — it usually tells you exactly what went wrong and where
- Reproduce the bug consistently before trying to fix it
- Print debugging is fine; the built-in debugger (pdb/browser devtools) is better
- Rubber duck debugging (explaining code out loud) works surprisingly well
- Version control (git bisect) helps isolate when a bug was introduced
Every developer spends a significant portion of their time debugging. The difference between a beginner and an experienced developer isn't that experienced developers write code without bugs — it's that they find and fix bugs faster. Debugging is a skill with learnable techniques. This guide covers the systematic approach that makes debugging methodical rather than random.
Read the Error Message: It's Telling You Something
Beginners scroll past error messages looking for their code. Don't. The error message is your first and most important clue. Python tracebacks read from bottom to top — the actual error is at the bottom, the call stack shows how you got there. Key parts: Exception type (TypeError, AttributeError, KeyError, etc.) tells you the category of problem. Message gives the specific issue. Line number and file show where it happened. Common Python errors decoded: AttributeError: 'NoneType' object has no attribute 'x' — something you expected to have a value is None. KeyError: 'name' — trying to access a dict key that doesn't exist. TypeError: unsupported operand type(s) for +: 'int' and 'str' — type mismatch.
Reproduce the Bug Before You Fix It
The worst thing you can do is start changing code before you can consistently reproduce the bug. If you can't reproduce it, you can't know when you've fixed it. Steps to a reliable reproduction: Minimize inputs — what's the smallest input that triggers the bug? Isolate the environment — does it happen in development but not production? In one browser but not another? Document the reproduction steps — write them down so you can repeat them exactly. Once you can reproduce consistently, you have a test case (even if informal) to verify your fix. This is also why writing a failing unit test before fixing a bug is valuable — the test becomes your proof of fix.
Print Debugging: Fast and Always Available
Print statements (or console.log in JavaScript) are legitimate debugging tools. Don't let anyone tell you otherwise. They're fast to add and work everywhere. Effective print debugging: print inputs AND outputs at the boundary of suspect functions. Print inside loops when debugging iteration issues. Use print(f'variable={variable!r}') — the !r uses repr() which shows string quotes and special characters clearly. In Python 3.8+: print(f'{variable=}') automatically prints variable=value. Clean up debug prints before committing — or use a logger set to DEBUG level that can be turned off.
Using a Debugger: Stop, Inspect, Understand
A debugger lets you pause execution and inspect program state — much more powerful than print statements for complex bugs. Python: import pdb; pdb.set_trace() (or breakpoint() in Python 3.7+). This drops you into an interactive REPL at that line. Commands: n (next line), s (step into function), c (continue), p variable (print value), l (show surrounding code), q (quit). Better: VS Code's Python debugger — set breakpoints by clicking the gutter, run in debug mode, inspect variables in the sidebar without typing commands. For JavaScript: Chrome DevTools Sources tab. Set breakpoints, step through code, inspect the call stack and local variables.
Binary Search Debugging: Isolate Bugs Systematically
For hard-to-find bugs, binary search is a systematic approach. Given a function with 100 lines that produces wrong output: add a print statement or assertion in the middle (line 50). If state is correct there, the bug is in lines 51-100. If wrong, the bug is in lines 1-50. Repeat. This finds the bug in O(log n) steps instead of reading every line. For git history: git bisect start, mark the last known-good commit and the current broken state, and git automatically checks out commits for you to test. It binary-searches your commit history to find exactly which commit introduced the bug. Works on any codebase and is invaluable for regressions.
Bug Prevention: Write Code That Fails Loudly
The best debugging is not needing to debug. Practices that prevent bugs: Type hints in Python — catch type errors before running. TypeScript for JavaScript — same reason. Assertions — assert isinstance(user_id, int), f'Expected int, got {type(user_id)}' — fail fast with a clear message. Unit tests — if you test your functions, bugs surface immediately when code changes break something. Linters (flake8, ESLint) catch common bugs automatically. Input validation at boundaries — validate data at API boundaries and user inputs before passing into your core logic. The principle: fail loudly and early rather than silently and late. A crash on bad input is better than silently computing wrong results.
Frequently Asked Questions
- What is the fastest way to find a bug?
- Read the full error message and stack trace carefully. If there's no error message, add assertions or print statements at the boundaries of your suspected area, then binary search inward. Reproduce the bug consistently before trying to fix it — if you can't reproduce it reliably, you can't know when you've fixed it.
- Should I use print statements or a debugger?
- Both are valid. Print statements are fast and work everywhere. A debugger is more powerful for complex bugs — you can inspect all variables at once, step through code, and evaluate expressions without restarting. Use whichever gets you to the bug faster. Remove debug output before committing.
- How do I debug code I didn't write?
- Start with the error message and stack trace to find the relevant code. Read the function signatures and docstrings. Add print statements at entry points to trace execution flow. Look for recent changes in git history near the code. Check if there are unit tests that document expected behavior.
- What is rubber duck debugging?
- Rubber duck debugging means explaining your code out loud to an inanimate object (traditionally a rubber duck). The act of explaining forces you to slow down and think step by step, which often surfaces the bug you were overlooking. It works because explaining to someone else — even an imaginary someone — engages a different mode of thinking than reading code.
Ready to Level Up Your Skills?
Debugging, testing, and writing production-quality code are all covered in our hands-on bootcamp. Real projects, real feedback. Next cohorts October 2026 in Denver, NYC, Dallas, LA, and Chicago. Only $1,490.
View Bootcamp Details