❮ Back to Journal

Debugging JavaScript Like a Pro: Essential Techniques for Beginners

Introduction

If you’re new to JavaScript, you’ve probably already experienced the frustration of code that doesn’t work as expected. Debugging is the process of finding and fixing these issues, and it’s a skill that separates beginners from experienced developers. In this guide, I’ll share practical debugging techniques that will help you solve problems faster and with less stress.

Understanding JavaScript Errors

Before we dive into debugging techniques, it’s important to understand the common types of errors you’ll encounter:

1. Syntax Errors

These occur when your code violates JavaScript’s grammar rules. The good news is that syntax errors prevent your code from running at all, so they’re usually caught immediately.

// ❌ Syntax Error Example
if (x === 5 {  // Missing closing parenthesis
  console.log("x is 5");
}

2. Reference Errors

These happen when you try to use a variable that doesn’t exist or is out of scope.

// ❌ Reference Error Example
console.log(undefinedVariable); // Variable doesn't exist

3. Type Errors

These occur when you try to perform an operation on a value of the wrong type.

// ❌ Type Error Example
const name = "John";
name.push("Doe"); // Error: name.push is not a function (strings don't have a push method)

4. Logic Errors

The most challenging errors to find - your code runs without throwing errors, but it doesn’t behave as expected.

// ❌ Logic Error Example
function calculateTotal(price, quantity) {
  return price + quantity; // Should be price * quantity
}

Essential Console Methods for Debugging

The console object provides several methods beyond just console.log() that can make debugging much easier:

console.log()

The most basic tool, but incredibly useful. Use it to check values at different points in your code:

function calculateDiscount(price, discountPercent) {
  console.log("Price:", price, "Discount:", discountPercent);

  const discount = price * (discountPercent / 100);
  console.log("Calculated discount:", discount);

  return price - discount;
}

console.table()

Perfect for visualizing arrays and objects:

const users = [
  { id: 1, name: "Alice", role: "Admin" },
  { id: 2, name: "Bob", role: "User" },
  { id: 3, name: "Charlie", role: "User" }
];

console.table(users);
// Displays a nicely formatted table with columns for each property

console.dir()

Shows all properties of an object in an interactive list:

const element = document.getElementById("myElement");
console.dir(element);
// Shows all properties and methods of the DOM element

console.group() and console.groupEnd()

Organize related logs into collapsible groups:

function processUser(user) {
  console.group(`Processing user: ${user.name}`);

  console.log("Checking permissions...");
  // Permission checking code

  console.log("Updating profile...");
  // Profile update code

  console.groupEnd();
}

console.time() and console.timeEnd()

Measure how long operations take:

console.time("Array processing");

const largeArray = Array(1000000).fill(0).map((_, i) => i);
const processed = largeArray.filter(num => num % 2 === 0);

console.timeEnd("Array processing");
// Outputs: "Array processing: 45ms" (or however long it took)

Using Browser Developer Tools

Modern browsers come with powerful developer tools that are essential for debugging:

The Sources Panel

This is where you’ll spend most of your debugging time:

  1. Open your browser’s dev tools (F12 or right-click → Inspect)
  2. Go to the Sources panel
  3. Find your JavaScript file in the file navigator
  4. Set breakpoints by clicking on line numbers

Setting Breakpoints

Breakpoints pause code execution at specific lines, allowing you to inspect the state at that moment:

function calculateTotal(items) {
  let total = 0;

  for (const item of items) {
    // Set a breakpoint on the next line to inspect each item
    total += item.price * item.quantity;
  }

  return total;
}

Stepping Through Code

Once execution is paused at a breakpoint, you can:

  • Step Over (F10): Execute the current line and move to the next one
  • Step Into (F11): If the current line contains a function call, jump into that function
  • Step Out (Shift+F11): Complete the execution of the current function and return to the caller
  • Continue (F8): Resume execution until the next breakpoint

Watch Expressions

Add expressions to the Watch panel to monitor their values as you step through code:

  1. In the Sources panel, find the “Watch” section
  2. Click the + button
  3. Enter any valid JavaScript expression (e.g., total, items.length, item.price * item.quantity)

The Call Stack

The call stack shows the path of function calls that led to the current point of execution. This helps you understand how your code reached a particular state.

Debugging Asynchronous Code

Asynchronous code (like Promises, async/await, and callbacks) adds complexity to debugging:

Async/Await

Using async/await makes debugging asynchronous code much easier:

async function fetchUserData(userId) {
  try {
    console.log("Fetching user data...");
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const userData = await response.json();
    console.log("User data received:", userData);
    return userData;
  } catch (error) {
    console.error("Error fetching user data:", error);
    throw error;
  }
}

Async Breakpoints

In Chrome DevTools, you can set special breakpoints for async operations:

  1. In the Sources panel, find the “Event Listener Breakpoints” section
  2. Expand “XHR/fetch” and check “Fetch” to pause when fetch requests occur

The Network Panel

For debugging API calls and other network requests:

  1. Open the Network panel in DevTools
  2. Filter by type (XHR, JS, CSS, etc.)
  3. Click on a request to see details including:
    • Headers
    • Request payload
    • Response data
    • Timing information

Common Debugging Scenarios and Solutions

Problem 1: “Cannot read property ‘x’ of undefined”

This common error occurs when you try to access a property on an undefined value:

// ❌ Problem
function displayUserName(user) {
  document.getElementById("username").textContent = user.name;
}

// If user is undefined, this will crash

// ✅ Solution: Add a check
function displayUserName(user) {
  if (!user) {
    console.error("User data is missing");
    return;
  }
  document.getElementById("username").textContent = user.name;
}

// Even better: Use optional chaining (ES2020)
function displayUserName(user) {
  document.getElementById("username").textContent = user?.name || "Guest";
}

Problem 2: Event Listeners Not Working

A common issue is adding event listeners to elements that don’t exist yet:

// ❌ Problem
document.getElementById("submit-button").addEventListener("click", submitForm);
// If the script runs before the button exists in the DOM, this will fail

// ✅ Solution 1: Move script to end of body
// <script src="app.js"></script> at the end of the body tag

// ✅ Solution 2: Use DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
  document.getElementById("submit-button").addEventListener("click", submitForm);
});

// ✅ Solution 3: Use event delegation
document.addEventListener("click", (event) => {
  if (event.target.id === "submit-button") {
    submitForm(event);
  }
});

Problem 3: Unexpected Loop Behavior

Loops can be tricky to debug, especially with closures:

// ❌ Problem: All buttons show the same number
for (var i = 0; i < 5; i++) {
  const button = document.createElement("button");
  button.textContent = `Button ${i}`;
  button.addEventListener("click", function() {
    console.log(`Button ${i} clicked`); // Always logs "Button 5 clicked"
  });
  document.body.appendChild(button);
}

// ✅ Solution 1: Use let instead of var (block scope)
for (let i = 0; i < 5; i++) {
  // Now each iteration has its own 'i'
  const button = document.createElement("button");
  button.textContent = `Button ${i}`;
  button.addEventListener("click", function() {
    console.log(`Button ${i} clicked`); // Works correctly
  });
  document.body.appendChild(button);
}

// ✅ Solution 2: Use a closure (for older browsers)
for (var i = 0; i < 5; i++) {
  const button = document.createElement("button");
  button.textContent = `Button ${i}`;
  button.addEventListener("click", (function(buttonIndex) {
    return function() {
      console.log(`Button ${buttonIndex} clicked`);
    };
  })(i));
  document.body.appendChild(button);
}

Debugging Workflow: A Step-by-Step Approach

When you encounter a bug, follow these steps:

  1. Reproduce the bug: Make sure you can consistently trigger the issue
  2. Isolate the problem: Narrow down where the bug is occurring
  3. Inspect the state: Use console.log or breakpoints to examine variables
  4. Form a hypothesis: Based on the evidence, guess what’s causing the issue
  5. Test your fix: Make a change and see if it resolves the problem
  6. Verify the solution: Make sure your fix doesn’t introduce new bugs

Debugging Tools Beyond the Browser

VS Code Debugger

VS Code has an excellent JavaScript debugger:

  1. Click the Debug icon in the sidebar
  2. Create a launch.json file (VS Code will help with this)
  3. Set breakpoints by clicking in the gutter next to line numbers
  4. Start debugging with F5

Linters and Type Checking

Prevent bugs before they happen:

  • ESLint: Catches potential issues and enforces code style
  • TypeScript: Adds static typing to JavaScript, catching type errors at compile time
// Install ESLint
// npm install eslint --save-dev

// TypeScript example
function calculateTotal(items: {price: number, quantity: number}[]): number {
  let total = 0;

  for (const item of items) {
    total += item.price * item.quantity;
  }

  return total;
}

Conclusion

Debugging is an essential skill that improves with practice. By understanding common error types, mastering console methods, and learning to use browser developer tools effectively, you’ll be able to solve problems more efficiently.

Remember these key points:

  • Use console.log() and other console methods strategically
  • Set breakpoints to pause execution and inspect state
  • Learn to read error messages carefully
  • Add checks for undefined or null values
  • Use async/await to make asynchronous code easier to debug
  • Follow a systematic debugging workflow

With these techniques in your toolkit, you’ll spend less time frustrated by bugs and more time building great JavaScript applications.

Further Resources