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
-
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
andlastName
rather thanstring1
andstring2
). It could also mean using built-in methods (such as Array's map) that hint at what return value to expect. - Similarly, try to have your functions fulfill one main purpose. This will reduce errors and make your code easier to follow.
- 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.
-
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.
- 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);
// => []
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
// => []
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
// => []
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' ]
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);
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.
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.
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.
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.
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.
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.
You should be able to see the values of currentStudent
, firstLetter
, i
, and jStudent
change as you move through the iterations.
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' ]
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)
that was quite the read going through alot of this at the point i am in my coding journey