How to Debug Code Like a Pro
Every developer spends a significant portion of their time debugging. The difference between junior and senior developers isn't that seniors write bug-free code — it's that they find and fix bugs faster. Here's how.
The Debugging Mindset
Before touching the code, adopt the right mindset:
1. The bug is real. The computer is doing exactly what you told it to do. The problem is in your code or your assumptions.
2. Reproduce first. If you can't reliably reproduce the bug, you can't verify you've fixed it.
3. One change at a time. If you change multiple things, you won't know which one fixed (or caused) the issue.
Step 1: Reproduce the Bug
Before debugging, create a reliable reproduction:
Bug Report
- Steps: Click "Save", then "Submit" twice rapidly
- Expected: Order submitted once
- Actual: Order submitted twice
- Environment: Chrome 120, macOS
If you can't reproduce it, gather more information. Check logs, ask users for exact steps, look for patterns.
Step 2: Isolate the Problem
Narrow down where the bug lives:
Binary Search Debugging
If you don't know where the bug is, cut the problem in half:
function processOrder(order) {
console.log('1: Input', order);
const validated = validateOrder(order);
console.log('2: After validation', validated);
const enriched = enrichOrder(validated);
console.log('3: After enrichment', enriched);
const result = saveOrder(enriched);
console.log('4: After save', result);
return result;
}
If the data is correct at point 2 but wrong at point 3, the bug is in enrichOrder.
Simplify the Input
Create the minimal input that still triggers the bug:
// Instead of debugging with production data
const hugeOrder = { / 200 fields / };
// Create minimal reproduction
const minimalOrder = {
id: 1,
items: [{ name: 'test', quantity: -1 }] // Just the problematic field
};
Step 3: Use the Right Tools
Console Methods Beyond console.log
// Group related logs
console.group('Order Processing');
console.log('Input:', order);
console.log('User:', user);
console.groupEnd();
// Table for arrays/objects
console.table(users);
// Timing
console.time('database');
await db.query();
console.timeEnd('database'); // database: 142ms
// Stack trace
console.trace('How did we get here?');
// Conditional logging
console.assert(user.id, 'User ID is missing!');
Debugger Statements
function calculateTotal(items) {
debugger; // Execution pauses here when DevTools is open
return items.reduce((sum, item) => sum + item.price, 0);
}
Browser DevTools
- Breakpoints: Click line numbers to pause execution
- Conditional breakpoints: Right-click → "Add conditional breakpoint"
- Watch expressions: Monitor specific variables
- Call stack: See how you got to the current line
- Network tab: Inspect API requests and responses
VS Code Debugger
// .vscode/launch.json
{
"type": "node",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/index.js",
"console": "integratedTerminal"
}
Then press F5 to start debugging with breakpoints, variable inspection, and step-through execution.
Step 4: Common Bug Patterns
Knowing common bugs helps you spot them faster:
Off-by-One Errors
// Bug: skips last element
for (let i = 0; i < array.length - 1; i++)
// Bug: goes one past the end
for (let i = 0; i <= array.length; i++)
// Correct
for (let i = 0; i < array.length; i++)
Null/Undefined Access
// Bug: crashes if user is null
const name = user.profile.name;
// Fix: optional chaining
const name = user?.profile?.name;
// Or explicit check
const name = user && user.profile && user.profile.name;
Async Timing Issues
// Bug: using data before it's loaded
fetchUser();
console.log(user); // undefined!
// Fix: await the result
const user = await fetchUser();
console.log(user);
Reference vs. Value
// Bug: mutating original array
const sorted = items.sort();
// Fix: create copy first
const sorted = [...items].sort();
Scope Issues
// Bug: var is function-scoped
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // Prints: 3, 3, 3
}
// Fix: use let (block-scoped)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // Prints: 0, 1, 2
}
Step 5: Rubber Duck Debugging
Explain the code out loud, line by line. The act of explaining often reveals the bug.
"First we get the user... then we check if they're logged in... wait, we're checking isLoggedIn before we actually fetch the user. That's the bug."
This works because explaining forces you to articulate assumptions you'd otherwise skip over.
Step 6: Take a Break
If you've been stuck for more than 30 minutes:
- Walk away for 10 minutes
- Explain the problem to someone else
- Work on something else and come back
- Sleep on it (seriously — your brain processes problems overnight)
Fresh eyes catch what tired eyes miss.
Step 7: Prevent Future Bugs
Once you fix the bug, ask:
1. Write a test. A regression test ensures this bug never comes back.
2. Find similar bugs. If you had an off-by-one error in one loop, check others.
3. Improve logging. If it took too long to find, add better logging.
4. Update documentation. If the behavior was confusing, clarify it.
Debugging Checklist
- [ ] Can I reproduce the bug reliably?
- [ ] Do I have error messages or stack traces?
- [ ] What changed recently? (check git log)
- [ ] What are my assumptions? Which might be wrong?
- [ ] Have I isolated the problem to a specific area?
- [ ] Am I debugging the right environment? (dev vs prod)
- [ ] Have I tried the simplest possible input?
- [ ] Have I taken a break if stuck?
Key Takeaway
Debugging is a skill you develop with practice. The best debuggers aren't necessarily the smartest — they're the most systematic. Follow a process, use your tools, and remember: every bug you find makes you a better developer.
Further Reading
For more debugging strategies, see How to Debug by John Regehr and Julia Evans' debugging resources.