Skillversitas

54. Debugging: JavaScript Debugging Techniques

Introduction

Debugging is the process of finding and fixing errors in code. JavaScript provides many built-in tools such as console methods, breakpoints, and the debugger statement that help trace the execution flow and find bugs. Chrome DevTools is the primary IDE for debugging JavaScript in the browser.

For IT students, strong debugging skills are essential. Most development time is spent fixing bugs, not writing new code. Mastering debugging techniques will save a lot of time.

Why Is Debugging Important?

Bugs that go unfound can cause application crashes, data corruption, or security vulnerabilities. Effective debugging helps you quickly find the root cause and understand how the code actually works, not how you think it works.

Console Methods

// console.log - basic output
const name = 'Mike';
const studentId = '1234567890';
console.log('Name:', name, 'StudentID:', studentId);

// Template literals in logs
console.log(`Student: ${name} (${studentId})`);

// console.warn - warning
console.warn('GPA below 2.5 requires academic attention');

// console.error - error
console.error('Failed to fetch data from server');

// console.table - display data in a table
const student = [
    { studentId: '001', name: 'Mike', gpa: 3.75, majorCode: 'CS' },
    { studentId: '002', name: 'John', gpa: 3.50, majorCode: 'IS' },
    { studentId: '003', name: 'Anna', gpa: 3.90, majorCode: 'CS' }
];
console.table(student);

// console.group and console.groupEnd - group output
console.group('Data Student');
console.log('Name:', name);
console.log('StudentID:', studentId);
console.group('Grade');
console.log('Midterm:', 85);
console.log('Final:', 90);
console.groupEnd();
console.groupEnd();

// console.time and console.timeEnd - measure execution time
console.time('Sort');
const sorted = [...Array(10000)].map(() => Math.random()).sort();
console.timeEnd('Sort'); // Sort: 12.5ms

// console.count - count invocations
function processData(data) {
    console.count('processData called');
    return data;
}

// console.assert - assert condition
const gpa = 3.5;
console.assert(gpa >= 0 && gpa <= 4, 'Invalid GPA:', gpa);

// console.dir - display an object in detail
console.dir(student[0]);

// console.trace - trace call stack
function functionC() {
    console.trace('Call stack:');
}
function functionB() { functionC(); }
function functionA() { functionB(); }
functionA();

Breakpoints

// Breakpoints in Chrome DevTools:
// 1. Line breakpoints: click a line number in the Sources tab
// 2. Conditional breakpoints: right-click -> Add conditional breakpoint
// 3. DOM breakpoints: right-click in the Elements tab -> Break on
// 4. XHR breakpoints: Sources tab -> XHR Breakpoints

// Example code to debug
function calculateGPA(gradeList) {
    let total = 0;                    // Breakpoint here
    let count = 0;

    for (const grade of gradeList) {
        total += grade;               // Watch: total
        count++;                      // Watch: count
    }

    const gpa = total / count;        // Step over
    return gpa;                       // Step into
}

// Conditional breakpoint example
// In DevTools, set the condition: grade === 100
// The breakpoint only activates when the condition is met

// Calculate the average
function calculateAverage(student) {
    const result = {};

    for (const [name, grade] of Object.entries(student)) {
        const avg = calculateGPA(grade);
        result[name] = avg;

        // Log for debugging
        console.log(`${name}: average = ${avg}`);
    }

    return result;
}

const studentData = {
    'Mike': [85, 90, 88, 92, 87],
    'John': [78, 82, 85, 80, 79],
    'Anna': [92, 95, 90, 88, 94]
};

console.log(calculateAverage(studentData));

Debugger Statement

// debugger statement: pauses execution at this point
function processEnrollment(student, course) {
    console.log('Memprocess enrollment...');

    debugger; // Execution stops here

    // In DevTools, you can:
    // - Inspect variables (Scope panel)
    // - Step through code (Step Over/Into/Out)
    // - Watch expressions
    // - Call stack

    if (!student.studentId) {
        throw new Error('Invalid StudentID');
    }

    debugger; // Second breakpoint

    const enrollment = {
        studentId: student.studentId,
        mkId: course.id,
        timestamp: new Date().toISOString(),
        status: 'active'
    };

    return enrollment;
}

// Debug async code
async function fetchData(url) {
    console.log(`Fetching: ${url}`);

    debugger;

    try {
        const response = await fetch(url);

        debugger; // Inspect response object

        const data = await response.json();
        return data;
    } catch (error) {
        debugger; // Inspect error object
        console.error('Fetch error:', error);
        throw error;
    }
}

Chrome DevTools Overview

// === Sources Panel ===
// - File navigator: browse source files
// - Code editor: view and edit code
// - Debug pane: watch, call stack, scope, breakpoints

// === Network Panel ===
// - View all HTTP requests
// - Inspect headers, payload, response
// - Filter by type: XHR, JS, CSS, img
// - Waterfall: timing of each request

// Example for debugging an API call
async function debugAPICall() {
    // Open the Network tab, filter "Fetch/XHR"
    const response = await fetch('/api/students/001');

    // Inspect the response in the Network tab
    const data = await response.json();
    console.log(data);
}

// === Console Panel ===
// - Interactive JavaScript console
// - $0 accesses the element selected in the Elements tab
// - copy() to copy to the clipboard
// - monitor(fn) to log every function call

monitor(fetch); // Will log all fetch calls
unmonitor(fetch);

// === Elements Panel ===
// - Inspect and edit the DOM
// - Computed styles
// - Event listeners
// - Break on mutations

Common Debugging Patterns

// 1. Debug: a failed API call
async function debugFetchAPI(url) {
    try {
        console.log(`[DEBUG] Fetching: ${url}`);

        const response = await fetch(url);

        console.log(`[DEBUG] Status: ${response.status} ${response.statusText}`);
        console.log(`[DEBUG] Headers:`, Object.fromEntries(response.headers));

        if (!response.ok) {
            const errorText = await response.text();
            console.error(`[DEBUG] Error body:`, errorText);
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();
        console.log(`[DEBUG] Response data:`, data);

        return data;
    } catch (error) {
        console.error(`[DEBUG] Fetch failed:`, error.message);
        console.error(`[DEBUG] Stack:`, error.stack);
        throw error;
    }
}

// 2. Debug: Variable inspection
function debugVariable(name, value) {
    console.log(`[DEBUG] ${name} =`, JSON.stringify(value, null, 2));
    console.log(`[DEBUG] ${name} type:`, typeof value);
    if (Array.isArray(value)) {
        console.log(`[DEBUG] ${name} length:`, value.length);
    }
    return value;
}

// Usage
const data = debugVariable('students', await fetchStudents());

// 3. Debug: Execution flow
function traceExecution(fn) {
    return function(...args) {
        console.group(`[TRACE] ${fn.name}(${args.map(a => JSON.stringify(a)).join(', ')})`);
        const result = fn.apply(this, args);
        console.log(`[TRACE] Result:`, result);
        console.groupEnd();
        return result;
    };
}

const tracedCalculateGPA = traceExecution(calculateGPA);
tracedCalculateGPA([85, 90, 88]);

// 4. Debug: State tracking
class StateTracker {
    #history = [];

    track(name, value) {
        this.#history.push({
            name,
            value: JSON.parse(JSON.stringify(value)),
            timestamp: new Date().toISOString()
        });
        console.log(`[STATE] ${name} =`, value);
    }

    getHistory() {
        return this.#history;
    }

    getHistoryFor(name) {
        return this.#history.filter(h => h.name === name);
    }

    diff(name) {
        const entries = this.getHistoryFor(name);
        if (entries.length < 2) return null;

        const prev = entries[entries.length - 2].value;
        const curr = entries[entries.length - 1].value;

        console.log(`[DIFF] ${name}:`, { before: prev, after: curr });
        return { before: prev, after: curr };
    }
}

// Usage
const tracker = new StateTracker();
tracker.track('gpa', 3.5);
tracker.track('gpa', 3.7);
tracker.diff('gpa'); // { before: 3.5, after: 3.7 }

// 5. Debug: Error boundary
function withDebugErrorBoundary(fn) {
    return async function(...args) {
        try {
            return await fn.apply(this, args);
        } catch (error) {
            console.group('[ERROR BOUNDARY]');
            console.error('Function:', fn.name);
            console.error('Arguments:', args);
            console.error('Error:', error.message);
            console.error('Stack:', error.stack);

            // Add context
            console.error('URL:', window.location.href);
            console.error('User Agent:', navigator.userAgent);
            console.error('Timestamp:', new Date().toISOString());
            console.groupEnd();

            throw error;
        }
    };
}

const safeFetch = withDebugErrorBoundary(debugFetchAPI);

Real-World Example: Debugging a Failed API Call

class APIDebugger {
    #baseURL;
    #logs;

    constructor(baseURL) {
        this.#baseURL = baseURL;
        this.#logs = [];
    }

    async request(method, path, options = {}) {
        const url = `${this.#baseURL}${path}`;
        const requestId = `req-${Date.now()}`;

        const logEntry = {
            requestId,
            method,
            url,
            startTime: performance.now(),
            request: {
                headers: options.headers || {},
                body: options.body
            }
        };

        console.group(`[API] ${method} ${url}`);
        console.log('Request ID:', requestId);

        try {
            console.time(requestId);

            const response = await fetch(url, {
                method,
                headers: {
                    'Content-Type': 'application/json',
                    ...options.headers
                },
                ...options
            });

            logEntry.status = response.status;
            logEntry.statusText = response.statusText;
            logEntry.responseHeaders = Object.fromEntries(response.headers);

            console.log('Status:', response.status, response.statusText);
            console.log('Response Headers:', logEntry.responseHeaders);

            let data;
            const contentType = response.headers.get('content-type');

            if (contentType?.includes('application/json')) {
                data = await response.json();
                console.log('Response Body:', data);
            } else {
                data = await response.text();
                console.log('Response Text:', data.substring(0, 200));
            }

            if (!response.ok) {
                console.error('API Error:', data);

                // Debug specific error types
                switch (response.status) {
                    case 401:
                        console.error('Auth error - token expired?');
                        console.log('Current token:', localStorage.getItem('token')?.substring(0, 20) + '...');
                        break;
                    case 403:
                        console.error('Permission denied - check user role');
                        break;
                    case 404:
                        console.error('Not found - check URL and params');
                        console.log('Requested path:', path);
                        break;
                    case 500:
                        console.error('Server error - check server logs');
                        break;
                }
            }

            logEntry.endTime = performance.now();
            logEntry.duration = logEntry.endTime - logEntry.startTime;

            console.timeEnd(requestId);
            console.log(`Duration: ${logEntry.duration.toFixed(2)}ms`);

            this.#logs.push(logEntry);
            console.groupEnd();

            if (!response.ok) {
                throw new Error(`API Error: ${response.status} ${response.statusText}`);
            }

            return data;
        } catch (error) {
            logEntry.error = error.message;
            logEntry.endTime = performance.now();
            logEntry.duration = logEntry.endTime - logEntry.startTime;

            console.error('Request failed:', error.message);
            console.error('Error type:', error.constructor.name);

            if (error.name === 'TypeError' && error.message.includes('fetch')) {
                console.error('Network error - check connectivity');
            }

            console.timeEnd(requestId);
            this.#logs.push(logEntry);
            console.groupEnd();

            throw error;
        }
    }

    getLogs() {
        return [...this.#logs];
    }

    getSlowRequests(thresholdMs = 1000) {
        return this.#logs.filter(log => log.duration > thresholdMs);
    }

    clearLogs() {
        this.#logs = [];
    }

    exportLogs() {
        const data = JSON.stringify(this.#logs, null, 2);
        console.log(data);
        return data;
    }
}

// Usage
const api = new APIDebugger('/api');

async function debugStudentFetch() {
    try {
        const students = await api.request('GET', '/students');
        console.log('Students loaded:', students.length);
    } catch (error) {
        console.error('Failed to load students');
    }

    try {
        const student = await api.request('GET', '/students/001');
    } catch (error) {
        // Will log the error in detail
    }

    // View all logs
    console.table(api.getLogs());

    // View slow requests
    console.log('Slow requests:', api.getSlowRequests(500));
}

Example: Tracing the Execution Flow

class ExecutionTracer {
    #traces;

    constructor() {
        this.#traces = [];
    }

    trace(name) {
        const traceId = `${name}-${Date.now()}`;
        const startMark = `${traceId}-start`;

        performance.mark(startMark);

        return {
            end: () => {
                const endMark = `${traceId}-end`;
                performance.mark(endMark);
                performance.measure(traceId, startMark, endMark);

                const measure = performance.getEntriesByName(traceId)[0];

                this.#traces.push({
                    name,
                    duration: measure.duration,
                    timestamp: new Date().toISOString()
                });

                console.log(`[TRACE] ${name}: ${measure.duration.toFixed(2)}ms`);

                return measure.duration;
            }
        };
    }

    async traceAsync(name, asyncFn) {
        const tracer = this.trace(name);
        try {
            const result = await asyncFn();
            tracer.end();
            return result;
        } catch (error) {
            tracer.end();
            console.error(`[TRACE] ${name} failed:`, error.message);
            throw error;
        }
    }

    getReport() {
        return this.#traces
            .sort((a, b) => b.duration - a.duration)
            .map(t => ({
                ...t,
                percentage: (t.duration / this.getTotalDuration() * 100).toFixed(1) + '%'
            }));
    }

    getTotalDuration() {
        return this.#traces.reduce((sum, t) => sum + t.duration, 0);
    }

    printReport() {
        console.group('[EXECUTION REPORT]');
        console.table(this.getReport());
        console.log('Total duration:', this.getTotalDuration().toFixed(2) + 'ms');
        console.groupEnd();
    }
}

// Usage
const tracer = new ExecutionTracer();

// Trace operations
async function loadDashboard() {
    await tracer.traceAsync('Fetch students', async () => {
        return fetch('/api/students').then(r => r.json());
    });

    const t1 = tracer.trace('Process data');
    // ... process data
    t1.end();

    await tracer.traceAsync('Render UI', async () => {
        // ... render
    });
}

// When done, view the report
await loadDashboard();
tracer.printReport();

Code Explanation

Console Methods

  • console.log() - Standard output
  • console.warn() - Warning (yellow)
  • console.error() - Error (red)
  • console.table() - Data in table format
  • console.group() / console.groupEnd() - Group output
  • console.time() / console.timeEnd() - Measure time
  • console.assert() - Assertion conditions
  • console.trace() - Display the call stack

Debugging Tools

  • Breakpoints: Pause execution on a specific line
  • Conditional breakpoints: Pause only when a condition is met
  • Watch expressions: Monitor variable values
  • Call stack: See the order of function calls
  • Scope: View variables in the current scope
  • Network tab: Inspect HTTP requests/responses
  • Performance tab: Analyze runtime performance

Patterns

  • Debug logging with a [DEBUG] prefix
  • Error boundaries to catch and log errors
  • Execution tracing to measure performance
  • API debugging to trace requests

Conclusion

Debugging is a fundamental skill every developer must master. Chrome DevTools provides a complete set of tools for debugging JavaScript: breakpoints to pause execution, console methods for logging, the Network tab for API debugging, and the Performance tab for optimization. For a campus application, make it a habit to add useful logging during development and remove it in production. Use the debugger statement to understand the execution flow, and the Performance API to measure bottlenecks.