The Art of Debugging
Debugging is one of the most important skills a developer can master. It is not just about fixing bugs - it is about understanding how your code actually behaves versus how you think it behaves. Effective debugging combines scientific thinking (hypothesis, experiment, observation) with deep knowledge of debugging tools. This topic covers JavaScript debugging from basic console techniques to advanced breakpoint strategies.
Console Methods Beyond console.log
// console.log with formatting
console.log('%cImportant Message', 'color: red; font-size: 20px; font-weight: bold');
console.log('%o', { deeply: { nested: { object: 'value' } } }); // expandable object
// console.table - display arrays/objects as sortable tables
const users = [
{ name: 'Alice', role: 'Admin', active: true },
{ name: 'Bob', role: 'User', active: false },
{ name: 'Carol', role: 'Editor', active: true },
];
console.table(users);
console.table(users, ['name', 'role']); // show only specific columns
// console.group / console.groupEnd - organize related logs
console.group('User Authentication');
console.log('Checking token...');
console.log('Token valid');
console.log('Loading user profile...');
console.groupEnd();
// console.groupCollapsed - same but starts collapsed
console.groupCollapsed('API Response Details');
console.log('Status: 200');
console.log('Headers:', headers);
console.log('Body:', body);
console.groupEnd();
// console.time / console.timeEnd - measure execution time
console.time('fetchUsers');
const data = await fetch('/api/users');
console.timeEnd('fetchUsers'); // "fetchUsers: 142.5ms"
// console.count - count how many times a code path is hit
function processItem(item: string) {
console.count('processItem called');
// ...
}
// "processItem called: 1", "processItem called: 2", etc.
// console.assert - log only when condition is false
console.assert(user.age >= 18, 'User must be an adult:', user);
// console.trace - print the call stack
function innerFunction() {
console.trace('How did we get here?');
}
// console.dir - inspect DOM elements as objects (not HTML)
console.dir(document.querySelector('.my-element'));
Breakpoint Debugging in Chrome DevTools
While console.log is useful for quick checks, breakpoints are far more powerful for understanding complex behavior. When execution pauses at a breakpoint, you can inspect every variable in scope, evaluate expressions, and step through code line by line.
Setting Breakpoints
- Line Breakpoint: Click the line number in Sources panel. Execution pauses before this line runs.
- Conditional Breakpoint: Right-click line number > "Add conditional breakpoint." Enter an expression like
user.id === 42. - Logpoint: Right-click line number > "Add logpoint." Enter a message like
'User:', user.name. Logs without pausing. - debugger statement: Add
debugger;directly in your code. DevTools pauses when it hits this line (only when DevTools is open).
Stepping Controls
# When paused at a breakpoint:
F8 (Cmd+\) # Resume execution (continue to next breakpoint)
F10 (Cmd+') # Step Over - execute current line, move to next
F11 (Cmd+;) # Step Into - enter the function call on this line
Shift+F11 # Step Out - run until current function returns
Cmd+Shift+P > "Restart frame" # Re-run the current function from the start
The Scope and Watch Panels
When paused at a breakpoint, the right sidebar shows:
- Scope: All variables in the current scope (Local, Closure, Script, Global). You can edit values by double-clicking.
- Watch: Custom expressions you want to monitor. Add expressions like
users.length,this.state, orerror?.message. - Call Stack: The chain of function calls that led to the current position. Click any frame to inspect its local scope.
- Breakpoints: List of all breakpoints. Toggle them on/off without removing them.
Advanced Debugging Techniques
The debugger Statement with Conditions
// Conditional debugger - only pause for specific conditions
function processOrder(order: Order) {
if (order.total > 1000) {
debugger; // Only pause for high-value orders
}
// ...
}
// Debug in catch blocks to inspect errors
try {
await riskyOperation();
} catch (error) {
debugger; // Pause here to inspect the error object
throw error;
}
XHR/Fetch Breakpoints
In the Sources panel > XHR/Fetch Breakpoints, add a URL pattern. The debugger pauses whenever a fetch or XHR request matches the pattern. This is invaluable for debugging API calls where you need to inspect the state when a request is made.
Event Listener Breakpoints
In the Sources panel > Event Listener Breakpoints, expand categories and check specific events. For example, check "Mouse > click" to pause on every click handler. This helps you find which code runs in response to user interactions.
Exception Breakpoints
Click the pause-on-exceptions button (octagonal icon) in the Sources panel to pause on all uncaught exceptions. Click it again to also pause on caught exceptions. This immediately shows you where errors originate, even if they are swallowed by a try/catch.
Debugging Async Code
// Chrome DevTools shows the full async call stack
// Set "Async" checkbox in Call Stack panel
async function loadDashboard() {
const user = await fetchUser(); // Set breakpoint here
const orders = await fetchOrders(user.id);
const analytics = await fetchAnalytics(user.id);
return { user, orders, analytics };
}
// The Call Stack will show:
// fetchUser <- current position
// loadDashboard <- async caller
// initApp <- original caller
// (async context preserved through await boundaries)
// Debugging Promise chains
fetchUser()
.then(user => {
debugger; // Pause here to inspect user
return fetchOrders(user.id);
})
.then(orders => {
debugger; // Pause here to inspect orders
return processOrders(orders);
})
.catch(error => {
debugger; // Pause here to inspect the error
console.error('Pipeline failed:', error);
});
Source Maps
Source maps connect your minified/transpiled production code back to the original source. When source maps are properly configured, you can debug TypeScript, JSX, and minified JavaScript as if you were running the original source. Chrome DevTools loads source maps automatically when they are referenced in the JavaScript file.
// tsconfig.json - Enable source maps
{
"compilerOptions": {
"sourceMap": true,
"declarationMap": true
}
}
// next.config.js - Source maps in Next.js
module.exports = {
productionBrowserSourceMaps: true, // Enable for production debugging
}
Debugging Best Practices
- Reproduce first: Before debugging, create a reliable way to reproduce the bug. If you cannot reproduce it, you cannot verify the fix.
- Binary search: If you do not know where the bug is, use breakpoints to binary search. Set one in the middle, determine which half has the bug, repeat.
- Read the error message: The stack trace tells you exactly where the error occurred. Read it from top to bottom.
- Check assumptions: Most bugs come from incorrect assumptions. Use breakpoints to verify each assumption.
- Remove console.log after debugging: Use logpoints instead - they do not require code changes and do not risk being committed.