
Stop console.log Debugging: Use the Debugger Like a Pro
You don't need 100 console.logs to find a bug. Learn to use browser debugger, breakpoints, and watch expressions effectively.

You don't need 100 console.logs to find a bug. Learn to use browser debugger, breakpoints, and watch expressions effectively.
Class is there, but style is missing? Debugging Tailwind CSS like a detective.

Think Android is easier than iOS? Meet Gradle Hell. Learn to fix minSdkVersion conflicts, Multidex limit errors, Namespace issues in Gradle 8.0, and master dependency analysis with `./gradlew dependencies`.

App crashed with TypeError? Learn why 'Null is not a subtype of String' happens and how to make your JSON parsing bulletproof with Zod/Freezed.

Clicked a button, but the parent DIV triggered too? Events bubble up like water. Understand Propagation and Delegation.

Two days ago, I hit a weird bug in our payment module. Sometimes when users clicked the payment button, they'd get charged twice. I scanned through the code but couldn't spot where things went wrong.
So I did what I always did... I started adding console.log() statements.
function handlePayment(amount) {
console.log('1. handlePayment called', amount);
if (!amount || amount <= 0) {
console.log('2. Invalid amount', amount);
return;
}
console.log('3. Validating user');
const user = getCurrentUser();
console.log('4. User:', user);
if (!user) {
console.log('5. No user found');
return;
}
console.log('6. Creating payment');
const payment = createPayment(user, amount);
console.log('7. Payment created:', payment);
console.log('8. Submitting to server');
submitPayment(payment);
console.log('9. Submit complete');
}
Nine console.logs in one function. And this was spread across five files. My console was flooded with logs, and I was scrolling up and down trying to trace what went wrong.
An hour passed. Logs went past 50. The funny thing? All the values in the logs looked normal. "Then where the hell is the bug??"
That's when my coworker walked by and casually said, "You'd find it in 10 seconds with the debugger..." That hit me like a truck.
What my coworker showed me was mind-blowing. He added one line debugger; to the code, ran it in the browser, and the execution paused right there. And at that exact moment, all variable values, the call stack, and scopes were neatly organized in the right panel.
With a few clicks, he stepped through the function line by line, watching in real-time exactly when values changed unexpectedly. What 50 console.logs couldn't show me, the debugger revealed in 3 minutes.
The problem was that submitPayment() was registering event listeners multiple times. The logs only showed the function being called once, but the call stack in the debugger clearly showed two event handlers stacked up.
This was real debugging. console.log felt like shining a flashlight at one spot at a time in a dark room, while the debugger was like turning on the lights for the entire room.
The simplest way is to put a debugger; statement in your code.
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
debugger; // Execution pauses here
return transformUserData(data);
}
When this code runs with DevTools open, execution pauses at the debugger; line. At this moment:
Putting debugger; in code is a temporary hack. You might forget to remove it before committing, and if you want to test multiple spots, you have to keep adding and removing it.
A better way is to click line numbers in the DevTools Sources tab to set breakpoints. A blue dot appears, and execution automatically pauses when it reaches that line.
Even more powerful is the conditional breakpoint. Right-click a line number to enter a condition. For example:
function processOrders(orders) {
orders.forEach((order, index) => {
// To pause only at the 100th order
// breakpoint condition: index === 99
calculateTotal(order);
applyDiscount(order);
finalizeOrder(order);
});
}
Even if the loop runs 1000 times, it only pauses when the condition matches. With console.log, you'd have to add if (index === 99) console.log(...) everywhere.
The Scope panel shows all variables, but sometimes there are too many and you can't find what you're looking for. That's when the Watch panel shines.
For example, if you want to track specific properties in a complex object:
function updateShoppingCart(cart, item) {
// Watch: cart.items.length
// Watch: cart.total
// Watch: item.price * item.quantity
cart.items.push(item);
cart.total += item.price * item.quantity;
if (cart.total > 100) {
cart.discount = cart.total * 0.1;
}
return cart;
}
Add expressions like cart.items.length and cart.total to the Watch panel, and these values update in real-time as you step through each line. Like watching formula results in Excel.
After pausing at a breakpoint, the most important thing is how you "move". The buttons at the top of DevTools:
Why does this matter? Here's an example from my experience:
function calculateOrderTotal(order) {
const subtotal = calculateSubtotal(order.items);
const tax = calculateTax(subtotal, order.region);
const shipping = calculateShipping(order.items, order.address);
debugger; // Pause here
return subtotal + tax + shipping;
}
When paused at debugger, calculateSubtotal, calculateTax, etc. have already executed. But if the subtotal value looks wrong? You need to go back and check.
Instead, set a breakpoint right before calculateSubtotal is called, and Step Into the function to see line by line what happens inside. You can track exactly where the bug originates.
One of the most powerful features is the Call Stack panel. It shows the entire path of function calls that led to the current paused point.
For example, if an error occurs deep in the code:
handleSubmit (button.js:45)
→ validateForm (form.js:120)
→ validateEmail (validators.js:67)
→ checkEmailFormat (validators.js:23) ← error here
Click on each level in the Call Stack to jump to that point and see the variable state at that moment. "What arguments were passed to this function?" You can actually verify it, not guess.
This is the decisive difference from console.log. Logs only show past snapshots, but the debugger pauses execution and shows the entire context of that moment.
In production, code is compressed and obfuscated by bundlers like Webpack or Vite. Does that make debugging impossible? No. Thanks to source maps.
Source maps are like a map connecting bundled code to original code. DevTools automatically reads them and shows your original TypeScript or JSX code.
Setup varies by bundler, but for Vite:
// vite.config.js
export default {
build: {
sourcemap: true, // Generate source maps even in production
},
}
This lets you debug deployed sites while viewing original code. Note that source map files are large, so exclude them in production if security is critical.
Browser DevTools are great, but debugging directly in VS Code is even better. Your code editor and debugger are on the same screen.
.vscode/launch.json configuration:
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true
}
]
}
Press F5 and Chrome launches automatically. You can set breakpoints, view variables, and step through code all in VS Code. No switching between browser and editor.
Same for Node.js backends. Change to "type": "node" and you can debug server code the same way.
Should we abandon console.log completely? No. There are right times for it:
console.time() and console.timeEnd() for performanceconsole.table() to view arrays or objects as tablesBut if you're adding 10, 20 console.logs because "I don't know where the bug is"? That's when you should use the debugger.
I finally understood the difference. console.log is based on guessing. You think "maybe the value looks wrong here?" and add a log. If wrong, you add another one somewhere else.
The debugger is based on observation. You pause the code and see what's actually happening. You see facts, not guesses.
At first, the debugger seemed complicated. So many buttons, so many panels. But after using it once or twice, it was actually simpler than console.log. The cycle of adding logs, refreshing, reading logs, adding more logs... I realized how inefficient that was.
Now when a bug appears, I start with a breakpoint first. Most problems reveal themselves in 3 minutes. I learned that one debugger statement is enough instead of 50 console.logs.