Debugging with Chrome DevTools

Master the Art of Finding and Fixing Bugs

Welcome to Your Debugging Toolkit

Chrome DevTools is like having X-ray vision for your web applications. Just as a doctor uses advanced imaging to diagnose issues inside the human body, developers use DevTools to peer inside their running applications, examine the code execution, and diagnose problems. Today, we'll learn how to become expert diagnosticians for our JavaScript applications!

Opening Chrome DevTools

There are several ways to open DevTools:

Pro tip: Ctrl+Shift+I (Windows/Linux) or Cmd+Opt+J (Mac) opens DevTools directly to the Console panel.

The DevTools Panels

graph TD A[Chrome DevTools] --> B[Elements] A --> C[Console] A --> D[Sources] A --> E[Network] A --> F[Performance] A --> G[Memory] A -> H[Application] A --> I[Security] A --> J[Lighthouse] B --> B1[DOM Inspector] B --> B2[CSS Editor] C --> C1[JavaScript REPL] C --> C2[Error Messages] D --> D1[File Navigator] D --> D2[Code Editor] D --> D3[Debugger] E --> E1[Request Monitor] E --> E2[Response Inspector] style D fill:#f9f,stroke:#333,stroke-width:4px style C fill:#f9f,stroke:#333,stroke-width:4px

Today we'll focus on the Console and Sources panels for JavaScript debugging.

The Console Panel

The Console is your JavaScript playground and error reporting center:

Console Methods

// Basic logging
console.log('Simple message');
console.info('Information message');
console.warn('Warning message');
console.error('Error message');

// Formatting output
console.log('%c Styled message', 'color: blue; font-size: 20px');
console.log('User %s has %d points', 'Alice', 150);

// Object inspection
const user = { name: 'Alice', age: 30, role: 'admin' };
console.log(user);
console.table(user);
console.dir(user);

// Grouping related logs
console.group('User Details');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.log('Role:', user.role);
console.groupEnd();

// Timing operations
console.time('fetchData');
fetchData().then(() => {
    console.timeEnd('fetchData');
});

// Counting occurrences
function processItem(item) {
    console.count('processItem called');
    // Process the item
}

// Assertions
console.assert(user.age > 0, 'Age must be positive');

// Stack traces
function outer() {
    inner();
}
function inner() {
    console.trace('Trace from inner function');
}
outer();

Console Shortcuts

The Sources Panel - Your Debugging Command Center

The Sources panel is where real debugging happens. It includes:

Setting Breakpoints

function calculateDiscount(price, discountPercent) {
    // Click on line numbers to set breakpoints
    const discount = price * (discountPercent / 100);
    const finalPrice = price - discount;
    
    return {
        originalPrice: price,
        discount: discount,
        finalPrice: finalPrice
    };
}

// Different types of breakpoints:
function processOrder(order) {
    // 1. Line breakpoint - stops on this line
    console.log('Processing order:', order.id);
    
    // 2. Conditional breakpoint - stops only when condition is true
    // Right-click line number → Add conditional breakpoint
    if (order.total > 1000) { // Set condition: order.total > 1000
        applyDiscount(order);
    }
    
    // 3. Logpoint - logs without stopping execution
    // Right-click line number → Add logpoint
    console.log('Order status:', order.status); // Logpoint message
    
    // 4. XHR/fetch breakpoint - stops on network requests
    fetch('/api/orders/' + order.id)
        .then(response => response.json())
        .then(data => console.log(data));
}

Debugging Controls

When execution pauses at a breakpoint, you have several controls:

graph LR A[Resume] -->|F8| B[Continue execution] C[Step Over] -->|F10| D[Execute current line] E[Step Into] -->|F11| F[Enter function call] G[Step Out] -->|Shift+F11| H[Exit current function] I[Step] -->|F9| J[Step by statement]

Watch Expressions

Add expressions to monitor their values as you debug:

// Example function to debug
function shoppingCart(items) {
    let total = 0;
    let itemCount = 0;
    
    // Add these to Watch panel:
    // total
    // itemCount
    // items.length
    // total / itemCount
    
    for (let item of items) {
        total += item.price * item.quantity;
        itemCount += item.quantity;
    }
    
    return {
        total: total,
        itemCount: itemCount,
        average: total / itemCount
    };
}

Call Stack and Scope

Understanding the call stack and variable scope is crucial for debugging:

function outer() {
    const outerVar = 'I am from outer';
    
    function middle() {
        const middleVar = 'I am from middle';
        
        function inner() {
            const innerVar = 'I am from inner';
            
            // Set breakpoint here
            console.log(outerVar);  // Accessible
            console.log(middleVar); // Accessible
            console.log(innerVar);  // Accessible
            
            debugger; // Programmatic breakpoint
        }
        
        inner();
    }
    
    middle();
}

outer();

// When paused at debugger:
// Call Stack shows: inner → middle → outer → (anonymous)
// Scope shows: Local, Closure (middle), Closure (outer), Global

Advanced Debugging Techniques

Event Listener Breakpoints

// Set up event listeners
document.getElementById('submitBtn').addEventListener('click', function(e) {
    // DevTools can break on specific event types
    console.log('Button clicked');
    submitForm();
});

// In DevTools:
// 1. Go to Sources panel
// 2. Expand Event Listener Breakpoints
// 3. Check 'Mouse → click'
// Now DevTools will pause whenever a click event fires

DOM Breakpoints

// You can set breakpoints on DOM modifications
const element = document.getElementById('myElement');

// In DevTools:
// 1. Right-click element in Elements panel
// 2. Break on → Subtree modifications
// 3. Now DevTools pauses when children are added/removed

// This will trigger the breakpoint
element.innerHTML = '<p>New content</p>';

Exception Breakpoints

// Enable "Pause on exceptions" in DevTools
function riskyOperation() {
    try {
        // This will throw an error
        JSON.parse('invalid json');
    } catch (e) {
        console.error('Caught error:', e);
    }
}

// With "Pause on exceptions" enabled:
// - Pauses when exception is thrown
// - Even if it's caught later
// - Helps find the source of errors

Debugging Asynchronous Code

Async Call Stack

// DevTools can track async operations
async function fetchUserData() {
    console.log('Fetching user data...');
    
    try {
        const response = await fetch('/api/user');
        const data = await response.json();
        
        console.log('User data received');
        return data;
    } catch (error) {
        console.error('Fetch failed:', error);
        throw error;
    }
}

// Enable "Async" in Call Stack
async function processUser() {
    const userData = await fetchUserData();
    // Set breakpoint here
    // Call Stack will show the async chain
    console.log('Processing user:', userData.name);
}

Promise Debugging

// Debugging promises
function delay(ms) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`Waited ${ms}ms`);
        }, ms);
    });
}

// Debug promise chains
delay(1000)
    .then(result => {
        console.log(result);
        return delay(500);
    })
    .then(result => {
        // Set breakpoint here
        console.log(result);
        return delay(250);
    })
    .catch(error => {
        console.error('Promise failed:', error);
    });

Performance Debugging

Performance Timing

// Measure code performance
function performanceTest() {
    // Using console.time
    console.time('Operation');
    heavyComputation();
    console.timeEnd('Operation');
    
    // Using performance.now
    const start = performance.now();
    heavyComputation();
    const end = performance.now();
    console.log(`Operation took ${end - start}ms`);
    
    // Using Performance API
    performance.mark('startOperation');
    heavyComputation();
    performance.mark('endOperation');
    performance.measure('My Operation', 'startOperation', 'endOperation');
    
    // View in Performance panel
    const measures = performance.getEntriesByType('measure');
    console.table(measures);
}

function heavyComputation() {
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
        result += Math.sqrt(i);
    }
    return result;
}

Memory Debugging

Finding Memory Leaks

// Common memory leak patterns
let leakyArray = [];

function createLeak() {
    // This creates a memory leak
    const largeObject = {
        data: new Array(1000000).fill('*'),
        id: Date.now()
    };
    
    leakyArray.push(largeObject);
    // Objects keep accumulating in memory
}

// Better approach
function noLeak() {
    const largeObject = {
        data: new Array(1000000).fill('*'),
        id: Date.now()
    };
    
    // Process the object
    processData(largeObject);
    
    // Object can be garbage collected after function exits
}

// Detecting leaks in DevTools:
// 1. Take heap snapshot
// 2. Perform action that might leak
// 3. Take another snapshot
// 4. Compare snapshots

Network Debugging

Debugging Network Requests

// Monitor network activity
async function fetchData() {
    try {
        // This request will appear in Network panel
        const response = await fetch('/api/data', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ query: 'test' })
        });
        
        // Check response status
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        console.log('Data received:', data);
        
    } catch (error) {
        console.error('Network error:', error);
    }
}

// Network panel features:
// - Filter requests by type
// - Examine request/response headers
// - Preview response data
// - Check timing information
// - Simulate slow connections

Real-World Debugging Scenarios

Scenario 1: Debugging a Form Submission

function validateForm(formData) {
    const errors = [];
    
    // Set breakpoint here to inspect formData
    if (!formData.email || !formData.email.includes('@')) {
        errors.push('Invalid email');
    }
    
    if (!formData.password || formData.password.length < 8) {
        errors.push('Password must be at least 8 characters');
    }
    
    // Watch expression: errors.length
    return errors;
}

function submitForm(event) {
    event.preventDefault();
    
    const formData = new FormData(event.target);
    const data = Object.fromEntries(formData);
    
    // Conditional breakpoint: errors.length > 0
    const errors = validateForm(data);
    
    if (errors.length > 0) {
        console.error('Validation errors:', errors);
        displayErrors(errors);
    } else {
        console.log('Form is valid, submitting...');
        sendToServer(data);
    }
}

Scenario 2: Debugging Async Data Flow

async function loadDashboardData() {
    try {
        console.group('Loading dashboard data');
        
        // Set breakpoint to examine each step
        const user = await fetchUser();
        console.log('User loaded:', user);
        
        const permissions = await fetchPermissions(user.id);
        console.log('Permissions loaded:', permissions);
        
        const dashboardData = await fetchDashboard(user.id, permissions);
        console.log('Dashboard data loaded:', dashboardData);
        
        console.groupEnd();
        
        renderDashboard(dashboardData);
        
    } catch (error) {
        // Exception breakpoint will pause here
        console.error('Dashboard loading failed:', error);
        showErrorMessage('Failed to load dashboard');
    }
}

Debugging Tips and Tricks

Practice Exercises

Exercise 1: Debug This Shopping Cart

// This code has bugs - use DevTools to find and fix them
function ShoppingCart() {
    this.items = [];
    this.total = 0;
}

ShoppingCart.prototype.addItem = function(item) {
    this.items.push(item);
    this.total += item.price; // Bug: doesn't account for quantity
};

ShoppingCart.prototype.removeItem = function(itemId) {
    const index = this.items.findIndex(item => item.id === itemId);
    if (index > 0) { // Bug: should be >= 0
        const item = this.items[index];
        this.total -= item.price * item.quantity;
        this.items.splice(index, 1);
    }
};

// Test the cart
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: 'Book', price: 20, quantity: 2 });
cart.addItem({ id: 2, name: 'Pen', price: 5, quantity: 1 });
cart.removeItem(1);

console.log('Total:', cart.total); // Should be 5, but isn't

Exercise 2: Debug Async Code

// Find why this async function sometimes fails
async function fetchDataWithRetry(url, maxRetries = 3) {
    let lastError;
    
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url);
            const data = await response.json();
            return data;
        } catch (error) {
            lastError = error;
            console.log(`Attempt ${i + 1} failed`);
        }
    }
    
    throw lastError; // Bug: lastError might be undefined
}

// Test with a failing URL
fetchDataWithRetry('https://api.invalid.com/data')
    .then(data => console.log('Success:', data))
    .catch(error => console.error('Failed:', error));

Key Takeaways

Additional Resources