Debugging JavaScript: The Guide I Wish I Had Earlier (2026)
Debugging is a skill. A learnable skill. Here's the systematic approach that saves hours of frustration.
Step 1: Reproduce the Bug
// Before fixing anything, you MUST reproduce it consistently
// ❌ "It sometimes doesn't work"
// ✅ "When I click the submit button after typing a special character,
// the form shows an error but only on Chrome 120+"
// Create a minimal reproduction:
// 1. What's the EXACT user action that triggers it?
// 2. What's the EXACT expected behavior?
// 3. What's the ACTUAL behavior?
// 4. What's different about this case vs working cases?
// Reproduction checklist:
const repro = {
browser: 'Chrome 120 / Firefox 121 / Safari 17',
os: 'macOS 14 / Windows 11 / Ubuntu 22.04',
device: 'desktop / mobile (iPhone 15)',
userState: 'logged in as admin / guest user',
data: 'user with special chars in name / empty state / >1000 items',
timing: 'first load / after 10 min idle / during API call',
network: 'fast WiFi / slow 3G / offline mode',
};
// If you can't reproduce it → add logging to capture when it happens
Step 2: Read the Error Message Carefully
// Most developers skip this step. Don't.
// Example error:
// TypeError: Cannot read properties of undefined (reading 'map')
// at UserProfile.jsx:23:15
// at renderWithHooks (react-dom.development.js:14985)
// ...
// Break it down:
// 1. Type of error: TypeError (wrong type being used)
// 2. What happened: Trying to read .map on undefined
// 3. Where: UserProfile.jsx line 23, column 15
// 4. Call stack: How we got there (most recent at top)
// Common error patterns and what they mean:
// TypeError: Cannot read properties of null/undefined
// → Something you expect to exist doesn't
// Fix: Add optional chaining or guard clause
data?.items?.map(item => ...) // ✅ Safe navigation
// TypeError: X is not a function
// → You're calling something that isn't a function
// Common causes: typo, wrong import, calling on wrong type
getUser() // ✅ Function
getUser // ❌ Not calling it (returns function itself)
obj.getUser() // ❌ obj.getUser might be undefined
// ReferenceError: X is not defined
// → Variable doesn't exist in current scope
// Common causes: typo, out of scope, not imported
const userId; // Declared but never assigned → undefined
// vs
userId; // Never declared → ReferenceError!
// SyntaxError: Unexpected token
// → Your code has a syntax error (usually missing bracket/paren)
// Check: matching {}, [], (), '', "", ; placement
// RangeError: Maximum call stack size exceeded
// → Infinite recursion or too deep nesting
function recurse() { return recurse(); } // Stack overflow!
Step 3: Console Methods You Should Use
// console.log is fine, but these are BETTER:
// console.table — Beautiful data display
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Carol', role: 'moderator' },
];
console.table(users);
// Shows formatted table with columns!
// console.group — Group related logs
console.group('API Call: /users');
console.log('Request:', { method: 'GET', url: '/users' });
console.log('Response:', userData);
console.log('Duration:', `${Date.now() - start}ms`);
console.groupEnd();
// console.assert — Conditional logging (only shows when WRONG)
console.assert(user.age >= 18, 'User is underage!', user.age);
// Only logs if age < 18
// console.time / timeEnd — Performance measurement
console.time('fetchUsers');
await fetchUsers();
console.timeEnd('fetchUsers'); // Output: fetchUsers: 142ms
// console.count — Count how many times something happens
function handleRequest(req) {
console.count('requests'); // Counts each call
}
// Output: requests: 1, requests: 2, requests: 3...
// console.trace — Show how you got here (call stack!)
function process(data) {
console.trace('Processing data');
// Shows full stack trace leading to this point
}
// console.dir — Inspect object properties deeply
console.log(document.body); // Shows DOM element briefly
console.dir(document.body); // Shows ALL properties and methods
// %o / %O / %s / %d / %i — Formatted output
console.log('User %s (ID: %d) is %o', name, id, userObj);
// CSS styling in console
console.log(
'%c ERROR! ',
'background: red; color: white; font-weight: bold; padding: 2px 6px;',
errorMessage
);
Step 4: Chrome DevTools Like a Pro
// === Network Tab ===
// Find failed API calls, check response status, inspect headers
// Filter by: Fetch/XHR for API calls only
// Right-click any request → Copy as curl → share with teammates
// === Sources Tab (Breakpoints) ===
// Click line number → set breakpoint (red dot)
// Execution pauses when that line runs
// Then you can:
// - Hover over variables to see their values
// - Use Console to run expressions in current scope
// - Step Over (F10): Execute next line
// - Step Into (F11): Dive into function call
// - Step Out (Shift+F11): Exit current function
// Conditional breakpoints (game changer!)
// Right-click breakpoint → Edit breakpoint → enter condition:
// user.id === 'problematic-user-id' // Only pause for this user
// items.length > 1000 // Only pause for large arrays
// response.status >= 400 // Only pause on errors
// Logpoint breakpoints (no pause, just log!)
// Right-click breakpoint → Add logpoint → expression:
// `User ID: ${user.id}, Items: ${items.length}`
// Logs to console without stopping execution!
// DOM breakpoints (catch code modifying DOM):
// Elements tab → right-click element → Break on:
// - Subtree modifications (child added/removed)
// - Attribute modifications (class changed)
// - Node removal (element deleted)
// === Call Stack Panel ===
// When paused, see HOW you got here
// Click any frame → jump to that code location
// Essential for understanding async code flow!
// === Memory Tab (for memory leaks) ===
// Heap snapshot: Compare before/after to find leaked objects
// Allocation sampling: See which functions allocate most memory
// Allocation instrumentation on timeline: Memory over time
// Quick workflow:
// 1. Take heap snapshot before action
// 2. Perform action (navigate, click, etc.)
// 3. Take another snapshot
// 4. Compare: Objects with +count between snapshots = potential leak
Step 5: Source Maps & Production Debugging
// Problem: Production code is minified (unreadable)
// Solution: Source maps link production code back to source
// webpack config:
devtool: 'source-map', // Full source map (accurate but large file)
// Options: eval-source-map (faster), cheap-source-map (smaller),
// hidden-source-map (no reference in bundle, for error tracking only)
// Error tracking with source maps:
// Sentry, Rollbar, Bugsnag all auto-apply source maps
// Result: Production errors show original file + line number!
// Manual source map usage:
// If you have an error from production:
// Error: TypeError at main.bundle.js:12345:12
// Use source map to find: src/components/UserForm.tsx line 45
// Generate source maps even for CLI tools:
// # sourceMappingURL=main.js.map
// Upload map to error tracking service (don't expose publicly!)
Step 6: Common Bugs & Their Fixes
// BUG #1: Async state updates (React example, but applies everywhere)
// ❌ State not updated yet
const [data, setData] = useState([]);
fetchData().then(result => {
console.log(data); // [] — still old value! setData hasn't re-rendered yet
});
// ✅ Use the result directly
fetchData().then(result => {
setData(result);
console.log(result); // Fresh data
});
// BUG #2: Closure in loop
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // All log 5!
}
// ✅ Fix: let (block scoped) or IIFE
for (let i = 0; i < 5; i++) { // let creates new binding per iteration
setTimeout(() => console.log(i), 100); // Logs 0, 1, 2, 3, 4
}
// BUG #3: Object mutation (shared reference)
const original = { items: [1, 2, 3] };
const copy = original;
copy.items.push(4); // Also modifies original!
// ✅ Deep clone
const safeCopy = JSON.parse(JSON.stringify(original));
// Or use structuredClone(obj) (modern, handles more types)
// BUG #4: Floating point math
0.1 + 0.2 !== 0.3 // true! (0.30000000000000004)
// ✅ Use appropriate comparison
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON // true
// Or use decimal libraries for financial calculations
// BUG #5: this context loss
const obj = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};
const fn = obj.greet;
fn(); // Hello, undefined — this is lost!
// ✅ Bind, arrow functions, or .call/.apply
fn.call(obj); // Hello, Alice
const bound = obj.greet.bind(obj);
bound(); // Hello, Alice
// BUG #6: Race conditions
let currentUser = null;
fetch('/api/user').then(u => { currentUser = u; });
fetch('/api/settings').then(s => {
console.log(currentUser); // Might be null if settings returns first!
});
// ✅ Chain promises properly
fetch('/api/user')
.then(u => { currentUser = u; return fetch('/api/settings'); })
.then(s => { console.log(currentUser); }); // Always defined
What's your favorite debugging technique? What bug took you longest to find?
Follow @armorbreak for more practical developer guides.
Top comments (0)