The Art of Debugging: How to Find Bugs Faster Than Everyone Else
A practical debugging guide for programmers — learn systematic debugging techniques, use DevTools and debuggers effectively, and find bugs faster with proven strategies.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
The Art of Debugging: How to Find Bugs Faster Than Everyone Else
The bug had been in production for three days. Certain users couldn't complete their purchases. Our error logs showed nothing. Monitoring showed nothing. The affected users were real and frustrated.
I spent four hours reproducing it. Then thirty minutes fixing it. The bug was a single character: a > that should have been >=. One character that cost the company real revenue and cost me half a workday.
What took those four hours wasn't the fix — it was finding the right line of code in 50,000 lines of a payment processing system. And I made every classic debugging mistake along the way: changing multiple things at once, guessing without evidence, and adding console.log statements in the wrong places.
In this guide, you'll learn the systematic approach to debugging that senior developers use — the one that would have found that bug in 30 minutes instead of four hours.
The Three Laws of Debugging
Before getting into tools and techniques, these three principles govern all effective debugging:
Law 1: Reproduce it reliably before touching anything. A bug you can't reproduce on demand is a bug you can't verify fixed. If you think you fixed a bug you couldn't reproduce, you might have just made it rarer.
Law 2: Change one thing at a time. If you change three things and the bug disappears, you don't know which change fixed it. Worse, two of the changes might have introduced new problems.
Law 3: Understand the fix before applying it. If you find a solution on Stack Overflow and it works but you don't know why, you haven't really fixed the bug — you've deferred it.
Phase 1: Reproduce the Bug
Make It Consistently Reproducible
Write down the exact steps to reproduce:
- What is the starting state?
- What are the exact inputs?
- What happens?
- What should happen?
If you can't reproduce it every time, find the conditions that make it happen. Is it time-dependent? Data-dependent? User-dependent? Race-condition-dependent?
Bug report: "Payment fails sometimes"
Not reproducible: trying to pay randomly until it fails
Reproducible:
- Log in as user with ID 447
- Add item "Widget Pro" to cart
- Enter card number 4242-4242-4242-4242
- Click pay
- Result: "Payment failed" error
- Expected: payment confirmation
The more specific the reproduction case, the easier the debugging.
Minimize the Reproduction Case
Strip away everything irrelevant until you have the smallest possible code that still demonstrates the bug. This technique is called finding the "minimal reproducing example."
// Original buggy code (200 lines)
async function processOrder(order) {
// ... 200 lines of complex business logic ...
}
// While debugging, you discover the bug is actually here:
function applyDiscount(price, discountCode) {
const discount = DISCOUNT_CODES[discountCode];
return price - price * discount; // Bug: NaN when discountCode is undefined
}
// Minimal example:
const DISCOUNT_CODES = { SAVE10: 0.1 };
console.log(applyDiscount(100, 'INVALID')); // NaN
A minimal reproducing example is often 90% of the solution — just finding it tells you what's wrong.
Phase 2: Understand the Bug
Read the Error Message Completely
Error messages contain the location and type of failure. Most developers skim them. Senior developers read them fully.
TypeError: Cannot read properties of undefined (reading 'email')
at sendWelcomeEmail (emailService.js:42:25)
at createUser (userService.js:18:3)
at POST /api/users (userRouter.js:31:5)
This tells you:
- What: Tried to read
.emailon something that isundefined - Where:
emailService.js, line 42, column 25 - Call path: Was called from
createUseratuserService.js:18, which was called from the POST/api/usershandler
Go to emailService.js:42. Find what's undefined. Usually that's the whole bug.
Use Binary Search on the Code
When you don't know where the bug is, don't read every line. Use binary search:
- Find the midpoint of your suspect code
- Add a checkpoint (log or breakpoint) there
- Is the data correct at that point? If yes, bug is in the second half. If no, bug is in the first half.
- Repeat on the relevant half
This finds the bug location in O(log n) time instead of O(n).
// Suspect: somewhere in this function, price is being set to NaN
function calculateTotal(items, discountCode, taxRate) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
console.log('CHECKPOINT 1: subtotal =', subtotal); // Is this right?
const discount = applyDiscount(subtotal, discountCode);
console.log('CHECKPOINT 2: after discount =', discount); // Is this right?
const tax = discount * taxRate;
const total = discount + tax;
console.log('CHECKPOINT 3: total =', total); // Is this right?
return total;
}
Checkpoints tell you exactly where the data goes wrong.
Phase 3: Use the Debugger
console.log debugging is the most common debugging approach — and the most limiting. Debuggers are dramatically more powerful.
Browser DevTools Debugger
Our Chrome DevTools guide covers the debugger in detail. The key concepts:
Breakpoints: Pause execution at a specific line. Inspect all variables in scope, the call stack, and the DOM state at that exact moment.
// Instead of:
console.log('user:', user);
console.log('order:', order);
console.log('total:', total);
// Set a breakpoint on the line you want to inspect.
// You can see ALL variables, not just ones you remembered to log.
Conditional breakpoints: Right-click a breakpoint → Add condition. The debugger only pauses when the condition is true — invaluable for loops:
// Only pause when the loop encounters the problematic item
// Condition: item.price === undefined
for (const item of items) {
total += item.price; // Set conditional breakpoint here
}
Watch expressions: Pin specific expressions to watch their values change as you step through code.
VS Code Debugger
For Node.js and server-side debugging, VS Code's built-in debugger connects directly to Node:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Server",
"program": "${workspaceFolder}/src/server.js",
"env": { "NODE_ENV": "development" }
}
]
}
Once configured: F5 to start, F9 to set breakpoints, F10 to step over, F11 to step into.
Phase 4: Common Bug Patterns and How to Find Them
Null/Undefined Reference Errors
The most common bug class. Prevention:
// Defensive checks
const email = user?.email ?? 'unknown';
// TypeScript helps catch these at compile time
function processUser(user: User | null) {
if (!user) return; // TypeScript forces you to handle null
sendEmail(user.email);
}
Off-By-One Errors
Classic in loops, array access, and pagination:
// Off by one — misses the last element
for (let i = 0; i < items.length - 1; i++) { }
// Correct
for (let i = 0; i < items.length; i++) { }
// Pagination off by one — skips first item on page 2
const offset = (page - 1) * pageSize; // page 1 → offset 0 ✓
// vs.
const offset = page * pageSize; // page 1 → offset pageSize ✗
Async/Await Bugs
// Bug: forgot await — returns Promise, not value
async function getUser(id) {
const user = db.findUser(id); // Missing await — user is a Promise object
return user.email; // TypeError: Cannot read property 'email' of Promise
}
// Fixed
async function getUser(id) {
const user = await db.findUser(id);
return user.email;
}
State Mutation Bugs
// Bug: mutating the original array
function addItem(cart, newItem) {
cart.items.push(newItem); // Mutates the original cart
return cart;
}
// Fixed: return new object
function addItem(cart, newItem) {
return {
...cart,
items: [...cart.items, newItem],
};
}
Phase 5: Prevention and Documentation
Write a Test for Every Bug You Fix
Before fixing a bug, write a test that fails because of the bug. Fix the bug. The test now passes. This test will catch any regression if the bug is ever reintroduced.
// Test documents the bug and proves the fix
test('applyDiscount returns original price when discount code is invalid', () => {
expect(applyDiscount(100, 'INVALID_CODE')).toBe(100); // Was returning NaN
expect(applyDiscount(100, undefined)).toBe(100);
expect(applyDiscount(100, null)).toBe(100);
});
Document Non-Obvious Fixes
When you fix a subtle bug, leave a comment explaining:
- What the bug was
- Why this fix works
- What would break if someone changes this code carelessly
This falls under the "why, not what" principle of our clean code guide.
Frequently Asked Questions
What is the most effective debugging technique?
Reproducing the bug reliably is the first step. Then use binary search — comment out half the suspect code, test, narrow down. The debugger with breakpoints is the most powerful tool once you've isolated the area.
Why do bugs appear in production but not locally?
Different environment variables, different data, different OS behavior, race conditions under real load, and cached values all differ between local and production environments.
What is rubber duck debugging and does it work?
Explaining your code out loud to an inanimate object forces you to verbalize assumptions. The act of articulating often reveals the wrong assumption. Research confirms this self-explanation effect improves problem-solving accuracy.
How do I debug code I didn't write?
Read documentation and comments. Check git blame for context. Run the code in a debugger to understand behavior. Write tests documenting your understanding before making changes.
What is the difference between a bug, error, and exception?
An error is unintended behavior (can be silent). An exception is a thrown runtime error (catchable). A bug is any defect encompassing both. Understanding the type helps choose the right debugging approach.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
15 Coding Habits That Separate Senior Developers from Juniors
Discover the coding habits senior developers follow every day — from writing readable code to debugging smarter — that separate pros from beginners.
The 10 VS Code Extensions That Make You Code Twice as Fast
The best VS Code extensions in 2025 that genuinely boost productivity — from AI code completion to live sharing, error highlighting, and formatting automation.
Terminal and Command Line Mastery: 30 Commands That Changed My Life
Learn essential terminal commands for developers — navigation, file operations, git, process management, and shell shortcuts that make you dramatically faster at the command line.
Data Structures for Humans: Finally Understanding Arrays, Trees, Graphs
Data structures explained simply for beginners — learn arrays, linked lists, stacks, queues, trees, and graphs with real-world analogies and practical code examples.