Advanced JavaScript (ES6+)

Debugging Techniques

13 min Lesson 36 of 40

Debugging Techniques

Debugging is a critical skill for every developer. In this lesson, we'll explore powerful debugging techniques and tools that will help you identify and fix bugs efficiently in your JavaScript code.

Console Methods Beyond console.log

While console.log is useful, JavaScript provides many more powerful console methods:

// console.error() - Display errors in red console.error('This is an error message'); // console.warn() - Display warnings in yellow console.warn('This is a warning'); // console.info() - Display informational messages console.info('Application version: 2.1.0'); // console.table() - Display arrays/objects as tables const users = [ { name: 'Alice', age: 25, city: 'New York' }, { name: 'Bob', age: 30, city: 'London' } ]; console.table(users); // console.group() - Group related logs console.group('User Details'); console.log('Name: Alice'); console.log('Age: 25'); console.groupEnd(); // console.time() - Measure execution time console.time('Loop Time'); for (let i = 0; i < 1000000; i++) { // Some operation } console.timeEnd('Loop Time'); // Loop Time: 5.234ms // console.trace() - Display stack trace function first() { second(); } function second() { console.trace('Trace point'); } first(); // console.assert() - Log only if condition is false const x = 5; console.assert(x === 10, 'x should be 10'); // console.count() - Count how many times called for (let i = 0; i < 3; i++) { console.count('Loop iteration'); } // console.dir() - Display object properties const element = document.querySelector('body'); console.dir(element); // Shows DOM properties
Tip: Use console.table() for arrays of objects - it provides a much clearer view than console.log(), especially when comparing multiple objects.

Browser DevTools Debugging

Modern browsers provide powerful debugging tools built right into the developer console:

Opening DevTools: - Chrome/Edge: F12 or Ctrl+Shift+I (Cmd+Option+I on Mac) - Firefox: F12 or Ctrl+Shift+I (Cmd+Option+I on Mac) - Safari: Cmd+Option+I (enable Developer menu first) Key DevTools Panels: 1. Console: - Execute JavaScript live - View logs, errors, warnings - Access variables in current scope - Use $0 to reference selected element 2. Sources/Debugger: - View and search source files - Set breakpoints - Step through code execution - Watch variables - View call stack 3. Network: - Monitor HTTP requests - View request/response headers - Check loading times - Inspect API calls 4. Performance: - Record runtime performance - Identify bottlenecks - Analyze frame rate - Memory usage profiling 5. Memory: - Take heap snapshots - Record allocations - Detect memory leaks - Compare snapshots

Breakpoints and Step Debugging

Breakpoints allow you to pause code execution at specific points and inspect the state:

// Setting breakpoints in code using debugger statement function calculateTotal(items) { let total = 0; debugger; // Execution will pause here when DevTools is open for (const item of items) { total += item.price * item.quantity; } return total; } // Types of breakpoints in DevTools: 1. Line Breakpoints: - Click line number in Sources panel - Execution pauses at that line 2. Conditional Breakpoints: - Right-click line number - Add condition (e.g., i === 50) - Pauses only when condition is true 3. DOM Breakpoints: - Right-click element in Elements panel - Break on attribute changes, node removal, or subtree modifications 4. XHR/Fetch Breakpoints: - Pause when specific URL is requested - Debug AJAX calls 5. Event Listener Breakpoints: - Pause on specific events (click, keydown, etc.) Step Controls: - Continue (F8): Resume execution to next breakpoint - Step Over (F10): Execute current line, don't enter functions - Step Into (F11): Enter function being called - Step Out (Shift+F11): Exit current function - Step: Execute next statement
Debugging Workflow: Set a breakpoint → Run code → Inspect variables → Step through execution → Identify issue → Fix and test.

Performance Profiling

Identify performance bottlenecks and optimize your code:

// Using Performance API const start = performance.now(); // Code to measure for (let i = 0; i < 1000000; i++) { // Some operation } const end = performance.now(); console.log(`Execution time: ${end - start}ms`); // Using performance.mark() and performance.measure() performance.mark('start-calculation'); // Complex calculation let result = 0; for (let i = 0; i < 1000000; i++) { result += Math.sqrt(i); } performance.mark('end-calculation'); performance.measure('calculation-time', 'start-calculation', 'end-calculation'); // Get the measurement const measure = performance.getEntriesByName('calculation-time')[0]; console.log(`Calculation took: ${measure.duration}ms`); // Profile different implementations function comparePerformance() { // Implementation 1: for loop console.time('For Loop'); let sum1 = 0; for (let i = 0; i < 1000000; i++) { sum1 += i; } console.timeEnd('For Loop'); // Implementation 2: reduce console.time('Reduce'); const arr = Array.from({ length: 1000000 }, (_, i) => i); const sum2 = arr.reduce((acc, val) => acc + val, 0); console.timeEnd('Reduce'); } // Using Chrome DevTools Performance Panel: // 1. Open DevTools > Performance tab // 2. Click Record button // 3. Perform actions you want to profile // 4. Click Stop // 5. Analyze flame graph and timeline

Memory Leak Detection

Memory leaks can severely impact application performance. Here's how to detect and prevent them:

// Common causes of memory leaks: 1. Forgotten Timers: // Bad: Timer never cleared const interval = setInterval(() => { console.log('Running...'); }, 1000); // Good: Clear timer when done const interval = setInterval(() => { console.log('Running...'); }, 1000); // Clear when component unmounts or no longer needed clearInterval(interval); 2. Event Listeners Not Removed: // Bad: Listener remains even after element removed function attachListener() { const button = document.getElementById('myButton'); button.addEventListener('click', handleClick); // Button removed but listener still in memory } // Good: Remove listener function attachListener() { const button = document.getElementById('myButton'); button.addEventListener('click', handleClick); // Later, when done: button.removeEventListener('click', handleClick); } 3. Closures Holding References: // Bad: Closure holds reference to large data function createProcessor() { const largeData = new Array(1000000).fill('data'); return function process(item) { // Only uses item, but holds reference to largeData return item.toUpperCase(); }; } // Good: Don't capture unnecessary variables function createProcessor() { return function process(item) { return item.toUpperCase(); }; } 4. Detached DOM Elements: // Bad: Reference to removed element let elements = []; function addElement() { const div = document.createElement('div'); document.body.appendChild(div); elements.push(div); // Keeps reference even after removal } // Good: Clean up references function removeElement(index) { const element = elements[index]; element.remove(); elements.splice(index, 1); // Remove reference } // Using Chrome DevTools Memory Panel: // 1. Take heap snapshot // 2. Perform action that might leak // 3. Take another snapshot // 4. Compare snapshots // 5. Look for objects that shouldn't persist
Warning: Always clean up event listeners, timers, and subscriptions when they're no longer needed. This is especially important in single-page applications.

Source Maps

Source maps allow you to debug minified or transpiled code by mapping it back to the original source:

// What are source maps? // - Map minified code to original source // - Enable debugging of production code // - Work with transpiled code (TypeScript, Babel) Enabling Source Maps: // In webpack.config.js module.exports = { devtool: 'source-map', // Production // or 'inline-source-map' for development }; // In Babel configuration { "sourceMaps": true } // In TypeScript tsconfig.json { "compilerOptions": { "sourceMap": true } } // Source map comment in generated file //# sourceMappingURL=bundle.js.map Benefits: - Debug with original variable names - See original file structure - Set breakpoints in source code - Better error messages - Easier stack traces

Best Debugging Practices

1. Reproduce the Bug: - Create minimal test case - Document steps to reproduce - Test in different environments 2. Isolate the Problem: - Use binary search (comment out half the code) - Test components independently - Check inputs and outputs 3. Understand Before Fixing: - Don't guess and check - Read error messages carefully - Use debugger to step through code 4. Use Descriptive Logging: // Bad console.log(data); // Good console.log('User data received:', { userId: data.id, timestamp: new Date() }); 5. Write Defensive Code: function processUser(user) { // Validate inputs if (!user || !user.id) { console.error('Invalid user object:', user); return null; } // Add assertions console.assert(typeof user.id === 'number', 'User ID must be number'); // Process... } 6. Use Error Boundaries: // Catch errors in specific parts of application try { riskyOperation(); } catch (error) { console.error('Operation failed:', error); // Handle gracefully } 7. Leverage Browser Extensions: - React DevTools - Vue DevTools - Redux DevTools - Lighthouse for performance

Practice Exercise:

Debug this code that has multiple issues:

// Buggy code function calculateAverage(numbers) { let total = 0; for (let i = 0; i <= numbers.length; i++) { total += numbers[i]; } return total / numbers.length; } const scores = [85, 90, 78, 92]; console.log(calculateAverage(scores));

Issues to find:

  1. Off-by-one error in loop condition
  2. Accessing undefined array element
  3. No input validation

Fixed code:

function calculateAverage(numbers) { // Validate input if (!Array.isArray(numbers) || numbers.length === 0) { console.error('Invalid input:', numbers); return 0; } let total = 0; // Fixed: Use < instead of <= for (let i = 0; i < numbers.length; i++) { // Validate each number if (typeof numbers[i] !== 'number') { console.warn(`Skipping non-number at index ${i}:`, numbers[i]); continue; } total += numbers[i]; } return total / numbers.length; } const scores = [85, 90, 78, 92]; console.log(calculateAverage(scores)); // 86.25

Summary

In this lesson, you learned:

  • Advanced console methods for better logging and debugging
  • How to use browser DevTools effectively for debugging
  • Setting and using different types of breakpoints
  • Performance profiling techniques and tools
  • How to detect and prevent memory leaks
  • Using source maps to debug production code
  • Best practices for systematic debugging
Next Up: In the next lesson, we'll master Regular Expressions for powerful text pattern matching and manipulation!