Back to blog

Debugging JavaScript: Advanced Techniques with DevTools You Need to Know

Hey HaWkers, debugging is one of the most important skills for any developer, but many still limit themselves to console.log(). Modern browser developer tools offer powerful features that can completely transform how you find and fix bugs.

Let's explore advanced techniques that go beyond the basics?

Beyond console.log()

console.log() is useful, but the console object offers much more:

Advanced Console Methods

// Group related logs
console.group('Processing User');
console.log('Name:', user.name);
console.log('Email:', user.email);
console.log('Permissions:', user.permissions);
console.groupEnd();

// Tables for arrays and objects
const users = [
  { name: 'Ana', age: 28, role: 'Dev' },
  { name: 'Carlos', age: 32, role: 'Lead' },
  { name: 'Maria', age: 25, role: 'Dev' }
];
console.table(users);

// Measure execution time
console.time('fetchUsers');
await fetchUsers();
console.timeEnd('fetchUsers'); // fetchUsers: 234.5ms

// Execution count
function processItem(item) {
  console.count('processItem called');
  // logic...
}

// Stack trace without error
function deepFunction() {
  console.trace('How did we get here?');
}

// Assertions for debugging
console.assert(user.id > 0, 'User ID should be positive', user);

Console Styling

// Colored and styled logs
console.log(
  '%c ERROR %c Authentication failed',
  'background: #ff0000; color: white; padding: 2px 6px; border-radius: 3px;',
  'color: #ff0000;'
);

console.log(
  '%c SUCCESS %c User created',
  'background: #00aa00; color: white; padding: 2px 6px; border-radius: 3px;',
  'color: #00aa00;'
);

// Multiple styles
console.log(
  '%cVariable:%c user.name %c=%c "John"',
  'color: gray;',
  'color: purple;',
  'color: gray;',
  'color: green;'
);

Conditional Breakpoints

Breakpoints are more powerful than simple code pauses.

Breakpoints with Conditions

In Chrome DevTools, right-click on a breakpoint and select "Edit breakpoint":

// Original code
async function processOrders(orders) {
  for (const order of orders) {
    await processOrder(order);
  }
}

// Condition in breakpoint: order.total > 1000
// Only pauses when order.total is greater than 1000

Logpoints (Breakpoints that don't pause)

Add logs without modifying code:

// In DevTools, add a logpoint with:
// "Processing order:", order.id, "total:", order.total

// Equivalent to adding console.log() in code,
// but without needing to modify and reload

DOM Breakpoints

Pause when DOM changes:

  1. Inspect the element in the Elements panel
  2. Right-click → Break on
  3. Choose: subtree modifications, attribute modifications, or node removal
// Useful for finding code that modifies elements unexpectedly
// DevTools will pause and show exactly which code made the modification

Debugging Asynchronous Code

Asynchronous code can be hard to debug. Here are specific techniques:

Async Stack Traces

Chrome DevTools shows the complete stack trace including asynchronous calls:

async function fetchUserData(userId) {
  // Breakpoint here shows the entire async path
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return data;
}

async function loadDashboard() {
  const user = await fetchUserData(currentUserId);
  await loadUserPosts(user.id);
  await loadUserNotifications(user.id);
}

// In the stack trace you'll see:
// - fetchUserData (async)
// - loadDashboard (async)
// - buttonClickHandler
// - (click event)

Promise Rejections

// Enable "Pause on caught exceptions" to debug rejected promises

async function riskyOperation() {
  try {
    const result = await mightFail();
    return result;
  } catch (error) {
    // With "Pause on caught exceptions" active,
    // the debugger stops here before catch executes
    console.error('Operation failed:', error);
    throw error;
  }
}

Event Listener Breakpoints

In the Sources panel → Event Listener Breakpoints:

// Mark specific events to pause:
// - Mouse → click
// - Keyboard → keydown
// - XHR/Fetch → Any XHR or fetch

// Useful for understanding event flow
// without needing to add breakpoints manually

Performance Profiling

Identifying performance bottlenecks is essential for responsive applications.

Performance Panel

// Start a recording in the Performance panel
// Execute the slow action
// Stop recording and analyze

// What to look for:
// - Long Tasks (tasks > 50ms)
// - Layout Thrashing (many reflows)
// - High scripting time
// - Blocking time

Identifying Layout Thrashing

// ❌ BAD - Causes multiple reflows
function updateElements(elements) {
  elements.forEach(el => {
    const height = el.offsetHeight; // Forces reflow (read)
    el.style.height = height + 10 + 'px'; // Forces reflow (write)
  });
}

// ✅ GOOD - Groups reads and writes
function updateElementsOptimized(elements) {
  // First, all reads
  const heights = elements.map(el => el.offsetHeight);

  // Then, all writes
  elements.forEach((el, i) => {
    el.style.height = heights[i] + 10 + 'px';
  });
}

Memory Profiling

// In the Memory panel, take snapshots to identify memory leaks

// Common memory leak pattern:
class Component {
  constructor() {
    // Event listener that is never removed
    window.addEventListener('resize', this.handleResize);
  }

  handleResize = () => {
    // ...
  };

  // ❌ Without cleanup, the listener keeps reference to component
}

// ✅ Fix:
class ComponentFixed {
  constructor() {
    window.addEventListener('resize', this.handleResize);
  }

  handleResize = () => {
    // ...
  };

  destroy() {
    window.removeEventListener('resize', this.handleResize);
  }
}

Source Maps and Minified Code Debugging

Configuring Source Maps

// webpack.config.js
module.exports = {
  devtool: 'source-map', // Production
  // or
  devtool: 'eval-source-map', // Development (faster)
};

// vite.config.js
export default {
  build: {
    sourcemap: true,
  },
};

Debugging with Source Maps

With source maps configured, you can:

  1. See original code in the Sources panel
  2. Add breakpoints in TypeScript/ES6+ code
  3. See variables with original names
  4. Stack traces point to original files
// Original TypeScript code
interface User {
  id: number;
  name: string;
}

async function getUser(id: number): Promise<User> {
  // Breakpoint here works normally
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Snippets and Overrides

Reusable Snippets

Create snippets in DevTools for repetitive tasks:

// Snippet: Clear localStorage and reload
(function clearAndReload() {
  localStorage.clear();
  sessionStorage.clear();
  location.reload();
})();

// Snippet: Export console data
(function exportConsoleData() {
  const data = /* your data */;
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'export.json';
  a.click();
})();

// Snippet: Monitor all fetch calls
(function monitorFetch() {
  const originalFetch = window.fetch;
  window.fetch = async (...args) => {
    console.group('Fetch Request');
    console.log('URL:', args[0]);
    console.log('Options:', args[1]);

    const start = performance.now();
    const response = await originalFetch(...args);
    const duration = performance.now() - start;

    console.log('Status:', response.status);
    console.log('Duration:', duration.toFixed(2) + 'ms');
    console.groupEnd();

    return response;
  };
  console.log('Fetch monitoring enabled');
})();

Local Overrides

Modify production files locally:

  1. In the Sources panel, create an overrides folder
  2. Select a file and edit
  3. Changes persist between reloads
  4. Useful for testing fixes in production without deploy

Network Debugging

Advanced Filters

// In the Network panel, use filters:

// By type
// is:running - Requests in progress
// larger-than:1M - Larger than 1MB
// status-code:500 - Only status 500

// By domain
// domain:api.example.com

// Negative expressions
// -domain:analytics.com - Exclude domain

// Multiple filters
// method:POST status-code:400

Throttling and Offline

// Simulate slow connections
// Network panel → No throttling → Select:
// - Slow 3G
// - Fast 3G
// - Offline

// Or create custom profiles:
// - Download: 100kb/s
// - Upload: 50kb/s
// - Latency: 500ms

Conclusion

Mastering debugging tools transforms your productivity as a developer. Instead of guessing where the problem is, you can go directly to the root cause.

Some recommended practices:

  • Use conditional breakpoints instead of multiple console.log()
  • Learn DevTools keyboard shortcuts
  • Use logpoints when you don't want to modify code
  • Profile regularly to identify performance issues before they become critical

The tools are available for free in all modern browsers. It's worth investing time to master them.

If you want to continue deepening your JavaScript knowledge, I recommend checking out the article Modern Browser Web APIs with JavaScript in 2025 where we explore powerful APIs that can improve your applications.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments