DEV Community

Alex Chen
Alex Chen

Posted on

Debugging JavaScript: The Guide I Wish I Had Earlier (2026)

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What's your favorite debugging technique? What bug took you longest to find?

Follow @armorbreak for more practical developer guides.

Top comments (0)