Debugging JavaScript: The Guide I Wish I Had Earlier (2026)
Debugging is a skill. A learnable skill. Here's how to find and fix bugs faster.
The Mindset
Before touching any tool:
1. Reproduce the bug consistently
→ Can you make it happen every time?
→ What are the EXACT steps?
→ What's different when it works vs when it doesn't?
2. Form a hypothesis
→ "I think it's because X is undefined here"
→ "I think the race condition happens between step 3 and 4"
→ Write down your hypothesis BEFORE investigating
3. Prove yourself wrong
→ The goal is NOT to confirm your guess
→ The goal is to DISPROVE it as fast as possible
→ If you can't disprove it, you're probably right
4. Fix the ROOT cause, not the symptom
→ Adding a try/catch = hiding the problem
→ Adding a setTimeout = racing with the bug, not fixing it
→ Understand WHY before fixing HOW
Console Techniques You Need
// 1. console.table — beautiful data display
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'moderator' },
];
console.table(users);
// Outputs a formatted table — way better than console.log!
// Filter columns:
console.table(users, ['name', 'role']);
// 2. console.group — organize related logs
function processOrder(order) {
console.group(`📦 Order ${order.id}`);
console.log('Customer:', order.customer);
console.log('Items:', order.items.length);
console.group('💰 Pricing');
console.log('Subtotal:', order.subtotal);
console.log('Tax:', order.tax);
console.log('Total:', order.total);
console.groupEnd();
console.groupEnd();
}
// 3. console.time / timeLog / timeEnd — measure performance
console.time('fetch-data');
fetch('/api/data')
.then(r => r.json())
.then(data => {
console.timeLog('fetch-data', 'received response'); // Intermediate timing
processData(data);
console.timeEnd('fetch-data'); // Final: "fetch-data: 234ms"
});
// 4. console.assert — silent unless condition fails
function validateUser(user) {
console.assert(user.email.includes('@'), 'Invalid email:', user.email);
console.assert(user.age >= 18, 'Underage user:', user.age);
// Only logs if assertion FAILS
}
// 5. console.trace — see the call stack
function updateConfig(key, value) {
if (key === 'dangerousSetting') {
console.trace('Dangerous setting changed!'); // Shows full call stack!
}
config[key] = value;
}
// 6. console.count — count how many times something runs
function handleRequest(req) {
console.count('requests'); // "requests: 1", "requests: 2", ...
// Reset: console.countReset('requests')
}
// 7. CSS styling in console
console.log(
'%c ERROR %c Something went wrong!',
'background: red; color: white; font-weight: bold; padding: 2px 8px;',
'color: red;'
);
// 8. String substitution (like printf!)
const user = { name: 'Alice', score: 95 };
console.log('User: %o scored %d points!', user, user.score);
// %s = string, %d/%i = number, %o = object, %O = expandable object, %f = float, %c = style
Breakpoints & Source Maps
// 1. debugger statement (the simplest breakpoint!)
function complexCalculation(a, b) {
const sum = a + b;
debugger; // Pauses execution HERE in DevTools
return sum * 2;
}
// Open DevTools → code pauses at this line → inspect all variables!
// 2. Conditional breakpoints (in DevTools Sources panel)
// Right-click line number → "Add conditional breakpoint"
// Enter: items.length > 1000
// Only pauses when condition is true (great for loops!)
// 3. Logpoint (non-breaking log)
// Right-click line number → "Add logpoint"
// Enter: 'Processing item:', itemName
// Logs without pausing execution!
// 4. XHR/Fetch breakpoints
// DevTools → Sources → XHR Breakpoints
// Add URL fragment: "/api/users"
// Pauses on ANY request matching that string
// 5. DOM breakpoint
// Elements panel → Right-click element → Break on:
// • Subtree modifications (child added/removed)
// • Attribute modifications (class, style changes)
// • Node removal (element deleted)
// 6. Event listener breakpoint
// DevTools → Sources → Event Listener Breakpoints
// Check "click" → pauses on ANY click event
// Great for finding where click handlers are registered!
Network Debugging
// 1. Copy as cURL (DevTools Network tab)
// Right-click request → Copy → Copy as cURL
// Paste in terminal to reproduce request exactly
// Modify and re-run to test API behavior
// 2. Override responses (DevTools Network panel overrides)
// Sources → Overrides → Enable local overrides
// Right-click network response → Save for overrides
// Edit the saved file → browser uses YOUR version instead!
// 3. Throttle network speed
// DevTools → Network → No throttling dropdown:
// Slow 3G: 500 Kbps upload, 50ms RTT (test real mobile experience!)
// Fast 3G: 1.5 Mbps upload, 150ms RTT
// Custom: Set exact speeds
// 4. Block specific requests
// DevTools → Network → Request blocking → Enable
// Add pattern: *.ads.js → blocks all ad scripts
// Test how your app behaves without third-party resources
// 5. Service Worker debugging
// Application → Service Workers
// Check "Update on reload" for development
// "Push" button to simulate push notifications
// "Sync" to test background sync
Memory Debugging
// 1. Heap snapshot (find memory leaks!)
// DevTools → Memory → Take heap snapshot
// Do operation that might leak → Take another snapshot
// Compare: Objects created but not freed = leak!
// 2. Allocation sampling (lightweight profiling)
// DevTools → Memory → Allocation sampling
// Run for 30 seconds while using app
// See which functions allocate most memory
// 3. Performance monitor (real-time metrics)
// DevTools → Rendering → Performance Monitor (Esc key)
// Watch: CPU usage, JS heap size, DOM nodes, event listeners
// If JS heap keeps growing → memory leak!
// Common memory leaks in JavaScript:
// Leak 1: Forgotten timers/intervals
// ❌ Component unmounts but interval keeps running
componentDidMount() {
this.timer = setInterval(() => this.updateData(), 1000);
}
// ✅ Cleanup on unmount
componentWillUnmount() {
clearInterval(this.timer);
}
// Leak 2: Event listeners not removed
// ❌
window.addEventListener('resize', this.handleResize);
// ✅
window.addEventListener('resize', this.handleResize);
// Later: window.removeEventListener('resize', this.handleResize);
// Leak 3: Closures holding references
function createHandler(element) {
let hugeData = new Array(1000000).fill('data');
return function handler() {
console.log('clicked'); // hugeData stays in memory forever!
};
}
// element.onclick = createHandler(el); // hugeData never GC'd!
// Leak 4: Detached DOM nodes
const list = document.getElementById('list');
list.innerHTML = ''; // Removes children from DOM...
// But if you hold references to them in JS, they can't be GC'd!
Common Bugs & How to Find Them
// Bug 1: "Cannot read property X of undefined"
// Cause: Chaining on possibly-undefined values
// Find: Look at the EXACT line in stack trace
// Fix: Optional chaining or guard clause
const userName = user?.profile?.name ?? 'Anonymous';
// or:
if (!user || !user.profile) return;
const name = user.profile.name;
// Bug 2: "X is not a function"
// Cause: Variable holds unexpected type
// Find: console.log(typeof variable, variable)
// Common: Imported wrong thing, async function result, property name typo
// Bug 3: Race conditions
// Symptom: Works sometimes, fails other times
// Find: Add timestamps to all async operations, look for ordering issues
// Fix: Proper sequencing (await chain), mutex/lock, or deduplication
// Bug 4: State mutation in React
// Symptom: UI doesn't update after data change
// Find: React DevTools → Components → check props/state
// Fix: Create NEW objects/arrays (never mutate directly!)
// Bug 5: Off-by-one errors
// Symptom: Missing first/last item, index out of bounds
// Find: Log array length and indices being accessed
// Use: Array.from({length: n}, (_, i) => i) to verify bounds
// Bug 6: Timezone/date bugs
// Symptom: Wrong date displayed, comparisons fail
// Find: console.log(date, date.toISOString(), date.getTime())
// Fix: Always use UTC internally, format for display only
const now = new Date();
const utcDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
Production Debugging
// 1. Error boundaries (React)
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack); // Send to error tracking!
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>; // Fallback UI
}
return this.props.children;
}
}
// Wrap your app: <ErrorBoundary><App /></ErrorBoundary>
// 2. Global error handlers
window.onerror = function(message, source, lineno, colno, error) {
trackError({
message,
source,
line: `${lineno}:${colno}`,
stack: error?.stack,
});
};
window.addEventListener('unhandledrejection', (event) => {
trackError({
type: 'unhandledRejection',
reason: String(event.reason),
stack: event.reason?.stack,
});
});
// 3. Structured logging
const logger = {
info: (msg, data) => console.log(JSON.stringify({ level: 'info', msg, ...data })),
error: (msg, err) => console.error(JSON.stringify({ level: 'error', msg, error: err.message, stack: err.stack })),
// In production, send to your logging service instead
};
// 4. Correlation IDs (trace requests across services)
const correlationId = crypto.randomUUID();
// Attach to ALL outgoing requests:
fetch(url, { headers: { 'X-Correlation-ID': correlationId } });
// When debugging: search logs by this ID to see the full request flow
What's your favorite debugging technique? What's the weirdest bug you've ever found?
Follow @armorbreak for more practical developer guides.
Top comments (0)