Beginner's Guide to Debugging (JavaScript)
Tags: javascript, debugging, webdev, codenewbie
Published: 2023-09-27 18:11:13


Table of Contents:
{%- # TOC start -%}

- [Printing to the Console](#printing-to-the-console)
- [Node Debugger](#node-debugger)
- [Chrome Developer Tools Debugger](#chrome-developer-tools-debugger)

{%- # TOC end -%}
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)) that hint at what return value to expect.
2. **Similarly, try to have your functions fulfill [one main purpose](https://codecoach.co.nz/functions-should-do-one-thing/).**
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Loops_and_iteration) 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'.
```javascript
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); 
// => []
```
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:

```javascript
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 
// => []
```

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.

```javascript
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 
// => []
```

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:

```javascript
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' ]
```

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](https://nodejs.org/en/download) 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.

```javascript
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);
```

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 `currentStudent`s is undefined, let's place the debugger statement near the `currentStudent` variable declaration.

```javascript
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;
}
```

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>
```

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'
```

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'
```

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'
```

All of the `i`s and `currentStudent`s 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
```

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.

```javascript
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' ]
```

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. 
```javascript
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);
```

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.
```javascript
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;
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qzdb0yxoqvmmwv439e0o.png)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbyy12y17w1etq1kxwxr.png)
You should be able to see the values of `currentStudent`, `firstLetter`, `i`, and `jStudent` change as you move through the iterations.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z23rxvrqg748cg6dfnmc.gif)
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 `{}`. ```javascript
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' ]
```

###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!