DEV Community

Cover image for Beginner's Guide to Debugging (JavaScript)
Kyra HP
Kyra HP

Posted on • Edited on

Beginner's Guide to Debugging (JavaScript)

Table of Contents:


If you're reading this, you might be a fellow Flatiron School student (👋 hello!) or staring anxiously at your code trying to figure out where it went wrong. Or, perhaps, you're just hoping to discover some useful techniques for debugging your code in the future. In this blog post, I'll review a few methods that I have used to debug my code along with some simple examples to follow along with.

But first, what is debugging and how will it be applicable to you?

Debugging is the process of determining why you are experiencing unexpected results or errors in your code. Regardless of whether you are a student trying to pass unit tests on an assignment or an experienced web developer, debugging will be a critical component of your coding journey. You should not expect all of your code to work on the first try, and errors do not mean that you are a bad developer! In fact, learning how to find bugs is an invaluable skill that will make you a better developer.

A few tips to make your code easier to debug

  1. Make your code as expressive, or as easy to understand, as possible. This could mean labeling variables and functions according to what they do (e.g. using variable names like firstName and lastName rather than string1 and string2). It could also mean using built-in methods (such as Array's map) that hint at what return value to expect.
  2. Similarly, try to have your functions fulfill one main purpose. This will reduce errors and make your code easier to follow.
  3. Declare variables and functions only where they're needed. If variables and functions are accessible by code that doesn't need them (e.g. if they were declared in the global scope unnecessarily), it will be much harder to trace bugs.
  4. Avoid mutating when possible. There are times when you will want to modify an Array or Object's state, but when possible, it's best to nondestructively update them (e.g. by creating copies before modifying). On a related note, it's also safest to declare your variables with const unless you already know that you will reassign the variable throughout your code. This will prevent unintentionally changing the value of a variable, and might instead produce a helpful error like this: Uncaught TypeError: Assignment to constant variable.
  5. Test your code often as you write it. It's much harder to find a bug when you're working with many untested features at a time.

Ok, so now that we've gotten that out of the way, here are a few debugger options. I'll be showing examples using a simple for-loop. Check out the MDN documentation if you need a refresher.

Printing to the Console

You can test your assumptions throughout your code using console.log() statements.

Let's say we have a function that's intended to take an array of students and return a new array with only the students whose names start with 'J'.

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i < array.length; i++) {
    const currentStudent = array[i];
    const firstLetter = currentStudent[1];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents); 
// => []
Enter fullscreen mode Exit fullscreen mode

When we run our function with our allStudents array passed in, it's returning an empty array. That's not what we were expecting - we wanted it to return ['Josh', 'Joseph', 'Jamie']. You might quickly see where we went wrong, or you might not (and that's ok!). Either way, bear with me while we add console.log() statements to debug this function.

One assumption I have about this code is that it's correctly accessing each element in the allStudents array. I can use console.log() to print each student's name in our original array, like so:

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i < array.length; i++) {
    const currentStudent = array[i];
    console.log(currentStudent); //ADDED LINE
    const firstLetter = currentStudent[1];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
// LOG: Alex
//      Bill
//      Josh
//      Sarah
//      Joseph
//      Jamie 
// => []
Enter fullscreen mode Exit fullscreen mode

Each student's name is correctly being printed to the console, so we know that our for-loop is set up correctly. Let's test another assumption - that we are correctly checking the first letter of each student's name. We can "see what the machine is thinking" (great quote courtesy of Flatiron School) when we access the first letter by printing our firstLetter variable.

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i < array.length; i++) {
    const currentStudent = array[i];
    const firstLetter = currentStudent[1];
    console.log(firstLetter); //ADDED LINE
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
// LOG: l
//      i
//      o
//      a
//      o
//      a 
// => []
Enter fullscreen mode Exit fullscreen mode

Here we can see that our assumption was wrong. We are accidentally checking the second letter of each name, and therefore firstLetter === 'J' is never true. We need to change the firstLetter assignment to currentStudent[0] instead of currentStudent[1]. Here's our final result:

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i < array.length; i++) {
    const currentStudent = array[i];
    const firstLetter = currentStudent[0];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
// => [ 'Josh', 'Joseph', 'Jamie' ]
Enter fullscreen mode Exit fullscreen mode

Although this is a fairly simple example, using console.log() to see values of variables, function calls, etc. is a valid way to trace more complex code.

Node Debugger

Now we are moving on to debug tools which allow us to "pause" our code and take a closer look at what's going on at each step. We'll start with the one provided by Node for the terminal (make sure Node is installed!).

First, use your terminal to navigate to the directory where your code is stored. I will use the same function from above, but this time with a different error.

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i <= array.length; i++) {
    const currentStudent = array[i];
    const firstLetter = currentStudent[0];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
Enter fullscreen mode Exit fullscreen mode

When we run this, we get the following error:

const firstLetter = currentStudent[0];
TypeError: Cannot read properties of undefined (reading '0')

Similar to using console.log(), we will choose a place in the code where we want to peek inside and see what's happening. In this case, we'll use the debugger keyword. Because we're seeing that one of the currentStudents is undefined, let's place the debugger statement near the currentStudent variable declaration.

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i <= array.length; i++) {
    const currentStudent = array[i];
    debugger; //ADDED LINE
    const firstLetter = currentStudent[0];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}
Enter fullscreen mode Exit fullscreen mode

From there, you can start the debugger with the command node inspect index.js in your terminal (use your file's name in place of index.js).

Now run cont at the debug prompt to start the loop. It will pause at your debugger breakpoint. Now we see something like this in our terminal:

break in index.js:7
  5   for (let i = 0; i <= array.length; i++) {
  6     const currentStudent = array[i];
> 7     debugger;
  8     const firstLetter = currentStudent[0];
  9     if (firstLetter === "J") {
debug>
Enter fullscreen mode Exit fullscreen mode

At this debug prompt, let's open the REPL by running repl. From there, we can check the values of variables by entering their names in the terminal. I'll start by checking the value of i and currentStudent.

debug> repl
Press Ctrl+C to leave debug repl
> i
0
> currentStudent
'Alex'
Enter fullscreen mode Exit fullscreen mode

This makes sense! We are in our first iteration of the for-loop, where i should be 0 and currentStudent should be 'Alex' (the first student in our allStudents array). So far so good. To continue stepping through the code to our next iteration, press Ctrl+C to exit the REPL and enter cont at the debug prompt again.

Now when I enter the REPL again, I should be in the second iteration of our for-loop. I can confirm this by checking the values of i and currentStudent again.

debug> repl
Press Ctrl+C to leave debug repl
> i
1
> currentStudent
'Bill'
Enter fullscreen mode Exit fullscreen mode

This also looks correct. I'm going to fast forward to the 6th iteration of our for-loop, where the currentStudent should be 'Jamie'.

debug> repl
Press Ctrl+C to leave debug repl
> i
5
> currentStudent
'Jamie'
Enter fullscreen mode Exit fullscreen mode

All of the is and currentStudents were as expected in each iteration. We still haven't reached an undefined currentStudent. If I step through the code again, I would expect the function to finish as we reached the last person in our allStudents array.

debug> cont
break in index.js:7
  5   for (let i = 0; i <= array.length; i++) {
  6     const currentStudent = array[i];
> 7     debugger;
  8     const firstLetter = currentStudent[0];
  9     if (firstLetter === "J") {
debug> repl
Press Ctrl+C to leave debug repl
> i
6
> currentStudent
undefined
Enter fullscreen mode Exit fullscreen mode

But that's not what happened. Here, the Node debugger showed us that there was another iteration left in our for-loop, which caused the i of 6 to be out of bounds. This is why currentStudent[0] was undefined - because there is no currentStudent at index 6. We can fix our code by removing the = from our for-loop condition.

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  const jStudents = [];
  for (let i = 0; i < array.length; i++) {
    const currentStudent = array[i];
    const firstLetter = currentStudent[0];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
// => [ 'Josh', 'Joseph', 'Jamie' ]
Enter fullscreen mode Exit fullscreen mode

The Node debugger is a great tool to check and debug your code, especially if you don't want to make your code messier with console.log() statements.

Chrome Developer Tools Debugger

Chrome's developer tools offer another debug tool that you can use, so long as you have an HTML file connected to your JavaScript file. You can make a basic HTML file that has the sole purpose of attaching your JS file. I attached my JS file by including the following line in my HTML file: <script type="text/javascript" src="index.js"></script>.

For this example, we'll use the same code as above with a slightly different error.

const allStudents = ["Alex", "Bill", "Josh", "Sarah", "Joseph", "Jamie"];

function startsWithJ(array) {
  {const jStudents = [];
    for (let i = 0; i < array.length; i++) {
      const currentStudent = array[i];
      const firstLetter = currentStudent[0];
      if (firstLetter === "J") {
        jStudents.push(currentStudent);
      }
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
Enter fullscreen mode Exit fullscreen mode

I'm going to open the HTML page in Chrome, then access the developer tools by pressing F12.

Dev tools has its own console, which is showing us an error right off the bat: Uncaught ReferenceError: jStudents is not defined. This is occurring at the end of our code where we're trying to return jStudents. Just like with the Node debug tool, I'm going to place a debugger statement in my code where I want to pause and take a closer look. I'll put it towards the beginning of my function.

function startsWithJ(array) {
  {const jStudents = [];
    debugger; //ADDED LINE
    for (let i = 0; i < array.length; i++) {
      const currentStudent = array[i];
      const firstLetter = currentStudent[0];
      if (firstLetter === "J") {
        jStudents.push(currentStudent);
      }
    }
  }
  return jStudents;
}
Enter fullscreen mode Exit fullscreen mode

Now, with the dev tools still open, I'm going to refresh the page. If not already open, go to the Source pane to see your code. The debugger should have paused the code at your debug statement.
If you look to the right, you should see your variable values and information on the call stack.

Image description
We can look here to see how the values of our variables (like jStudent) change as we proceed through the code. Instead of typing cont at a debug prompt in terminal, the nice UI of Chrome's debug tool allows us to press buttons to step through the code.
Image description
You should be able to see the values of currentStudent, firstLetter, i, and jStudent change as you move through the iterations.
Image description
Fast forwarding to the last iteration, jStudents is correctly assigned ['Josh', 'Joseph', 'Jamie']. We know that our for-loop is working properly. But when we continue stepping through each line and we exit the for-loop, you'll notice that jStudents disappeared from our scope tab. jStudents was listed as a block-level variable previously, which suggests it may have accidentally been declared in a scope that we no longer have access to.

Taking a closer look at our code, we can see that our return statement is outside of the code block ({}) where jStudents was declared. To fix our problem, we can put our variable declaration and return statement in the same scope by removing the unnecessary {}.

function startsWithJ(array) {
  const jStudents = [];
  debugger; //ADDED LINE
  for (let i = 0; i < array.length; i++) {
    const currentStudent = array[i];
    const firstLetter = currentStudent[0];
    if (firstLetter === "J") {
      jStudents.push(currentStudent);
    }
  }
  return jStudents;
}

startsWithJ(allStudents);
// => [ 'Josh', 'Joseph', 'Jamie' ]
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

The Chrome debugger is my personal favorite out of these options. I appreciate not having to clear out a bunch of console.log()s when I'm finished, and I think it's a lot more intuitive to use than Node's debugger. Any of these are valid ways to debug your code. You may even want to look into using a debugger built into your development IDE.

You now have the tools to tackle your bugs and thoroughly examine your code. Thanks for reading and good luck!

Top comments (1)

Collapse
 
clateman profile image
Clayton Malarkey

that was quite the read going through alot of this at the point i am in my coding journey