Debugging JavaScript Projects with VS Code & Chrome Debugger

Debugging JavaScript isn’t the most fun aspect of JavaScript programming, but it’s a vital skill. This article covers two tools that will help you debug JavaScript like a pro.

Imagine for a moment that the console.log() function did not exist in JavaScript. I’m pretty sure the first question you’d ask yourself would be “How am I ever going to confirm my code is working correctly?”

The answer lies in using debugging tools. For a long time, most developers, including myself, have been using console.log to debug broken code. It’s quick and easy to use. However, things can get finicky at times if you don’t know where and what is causing the bug. Often you’ll find yourself laying down console.log traps all over your code to see which one will reveal the culprit.

--ADVERTISEMENT--

To remedy this, we need to change our habits and start using debugging tools. There are a number of tools available for debugging JavaScript code, such as the Chrome Dev Tools, Node Debugger, Node Inspect and others. In fact, every major browser provides its own tools.

In this this article, we’ll look at how to use the debugging facilities provided by Visual Studio Code. We’ll also look at how to use the Debugger for Chrome extension that allows VS Code to integrate with Chrome Dev Tools. Once we’re finished, you’ll never want to use a console.log() again.

Prerequisites

For this tutorial, you only need to have a solid foundation in modern JavaScript. We’ll also look at how we can debug a test written using Mocha and Chai. We’ll be using a broken project, debug-example, to learn how to fix various bugs without using a single console.log. You’ll need the following to follow along:

Start by cloning the debug-example project to your workspace. Open the project in VS Code and install the dependencies via the integrated terminal:

# Install package dependencies
npm install

# Install global dependencies
npm install -g mocha

Now we’re ready to learn how to debug a JavaScript project in VS Code.

Debugging JavaScript in VS Code

The first file I’d like you to look at is src/places.js. You’ll need to open the debug-project folder in VS Code (File > Open Folder) and select the file from within the editor.

const places = [];

module.exports = {
  places,

  addPlace: (city, country) => {
    const id = ++places.length;
    let numType = 'odd';
    if (id % 2) {
      numType = 'even';
    }
    places.push({
      id, city, country, numType,
    });
  },
};

The code is pretty simple, and if you have enough experience in coding you might notice it has a couple of bugs. If you do notice them, please ignore them. If not, perfect. Let’s add a few of lines at the bottom to manually test the code:

module.exports.addPlace('Mombasa', 'Kenya');
module.exports.addPlace('Kingston', 'Jamaica');
module.exports.addPlace('Cape Town', 'South Africa');

Now, I’m sure you’re itching to do a console.log to output the value of places. But let’s not do that. Instead, let’s add breakpoints. Simply add them by left-clicking on the gutter — that is, the blank space next to the line numbers:

Red dots indicating breakpoints

See the red dots on the side? Those are the breakpoints. A breakpoint is simply a visual indication telling the debugger tool where to pause execution. Next, on the action bar, click the debug button (the icon that says “No Bugs Allowed”).

The debug panel

Look at the top section. You’ll notice there’s a gear icon with a red dot. Simply click on it. A debug configuration file, launch.json, will be created for you. Update the config like this so that you can run VS Code’s debugger on places.js:

"configurations": [
  {
    "type": "node",
    "request": "launch",
    "name": "Launch Places",
    "program": "${workspaceFolder}\\src\\places.js"
  }
]

Note: Depending on your operating system, you might have to replace the double backslash (\\) with a single forward slash (/).

After you’ve saved the file, you’ll notice that the debug panel has a new dropdown, Launch Places. To run it, you can:

  • hit the Green Play button on the debug panel
  • press F5
  • click Debug > Start Debugging on the menu bar.

Use whatever method you like and observe the debug process in action:

Debug running

A number of things happen in quick succession once you hit the debug button. First, there’s a toolbar that appears at the top of the editor. It has the following controls:

  • Drag Dots anchor: for moving the toolbar to somewhere it’s not blocking anything
  • Continue: continue the debugging session
  • Step over: execute code line by line, skipping functions
  • Step into: execute code line by line, going inside functions
  • Step out: if already inside a function, this command will take you out
  • Restart: restarts the debugging session
  • Stop: stops the debugging session.

Right now, you’ll notice that the debug session has paused on your first breakpoint. To continue the session, just hit the Continue button, which will cause execution to continue until it reaches the second breakpoint and pause again. Hitting Continue again will complete the execution and the debugging session will complete.

Let’s start the debugging process again by hitting F5. Make sure the two breakpoints are still in place. When you place a breakpoint, the code pauses at the specified line. It doesn’t execute that line unless you hit Continue (F5) or Step Over (F10). Before you hit anything, let’s take a look at the sections that make up the debug panel:

  • Variables: displays local and global variables within the current scope (i.e. at the point of execution)
  • Watch: you can manually add expressions of variables you want to monitor
  • Call Stack: displays a call stack of the highlighted code
  • Breakpoints: displays a list of files with breakpoints, along with their line numbers.

To add an expression to the Watch section, simply click the + sign and add any valid JavaScript expression — such as places.length. When the debugger pauses, if your expression is in scope, the value will be printed out. You can also hover over variables that are currently in scope. A popup will appear displaying their values.

Currently the places array is empty. Press any navigation control to see how debugging works. For example, Step over will jump into the next line, while Step into will navigate to the addPlace function. Take a bit of time to get familiar with the controls.

As soon as you’ve done some stepping, hover over the places variable. A popup will appear. Expand the values inside until you have a similar view:

The places popup

You can also inspect all variables that are in scope in the Variables section.

The variables section

That’s pretty awesome compared to what we normally do with console.log. The debugger allows us to inspect variables at a deeper level. You may have also noticed a couple of problems with the places array output:

  1. there are multiple blanks in the array — that is, places[0] and places[2] are undefined
  2. the numType property displays even for odd id values.

For now, just end the debugging session. We’ll fix them in the next section.

Debugging Tests with Mocha

Open test/placesTest.js and review the code that’s been written to test the code in places.test. If you’ve never used Mocha before, you need to install it globally first in order to run the tests.

# Install mocha globally
npm install -g mocha

# Run mocha tests
mocha

You can also run npm test to execute the tests. You should get the following output:

Failing mocha tests

All the tests are failing. To find out the problem, we’re going to run the tests in debug mode. To do that, we need a new configuration. Go to the debug panel and click the dropdown to access the Add Configuration option:

Add configuration

The launch.json file will open for you with a popup listing several configurations for you to choose from.

Add mocha tests

Simply select Mocha Tests. The following configuration will be inserted for you:

{
  "type": "node",
  "request": "launch",
  "name": "Mocha Tests",
  "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
  "args": [
    "-u",
    "tdd",
    "--timeout",
    "999999",
    "--colors",
    "${workspaceFolder}/test"
  ],
  "internalConsoleOptions": "openOnSessionStart"
},

The default settings are fine. Go back to the dropdown and select Mocha Tests. You’ll need to comment out the last three lines you added in places.js; otherwise the tests won’t run as expected. Go back to placesTest.js and add a breakpoint on the line just before where the first test failure occurs. That should be line seven, where it says:

addPlace('Nairobi', 'Kenya');

Make sure to add a places.length expression in the watch section. Hit the Play button to start the debugging session.

Mocha test start

At the start of the test, places.length should read zero. If you hit Step over, places.length reads 2, yet only one place has been added. How can that be?

Restart the debugging session, and this time use Step into to navigate to the addPlace function. The debugger will navigate you to places.js. The value of places.length is still zero. Click Step over to execute the current line.

Mocha test step

Aha! The value of places.length just incremented by 1, yet we haven’t added anything to the array. The problem is caused by the ++ operator which is mutating the array’s length. To fix this, simply replace the line with:

const id = places.length + 1;

This way, we can safely get the value of id without changing the value of places.length. While we’re still in debug mode, let’s try to fix another problem where the numType property is given the value even while id is 1. The problem seems to be the modulus expression inside the if statement:

Mocha if error

Let’s do a quick experiment using the debug console. Start typing a proper expression for the if statement:

Mocha debug console

The debug console is similar to the browser console. It allows you to perform experiments using variables that are currently in scope. By trying out a few ideas in the console, you can easily find the solution without ever leaving the editor. Let’s now fix the failing if statement:

if (id % 2 === 0) {
  numType = 'even';
}

Restart the debug session and hit Continue to skip the current breakpoint. The first test, “can add a place”, is now passing. But the second test isn’t. To fix this, we need another breakpoint. Remove the current one and place a new breakpoint on line 16, where it says:

addPlace('Cape Town', 'South Africa');

Start a new debugging session:

Mocha test2 fail

There! Look at the Variables section. Even before the second test begins we discover that the places array already has existing data created by the first test. This has obviously polluted our current test. To fix this, we need to implement some kind of setup function that resets the places array for each test. To do this in Mocha, just add the following code before the tests:

beforeEach(() => {
  places.length = 0;
});

Restart the debugger and let it pause on the breakpoint. Now the places array has a clean state. This should allow our test to run unpolluted. Just click Continue to let the rest of the test code execute.

Mocha tests passed

All tests are now passing. You should feel pretty awesome, since you’ve learned how to debug code without writing a single line of console.log. Let’s now look at how to debug client-side code using the browser.

Debugging JavaScript with Chrome Debugger

Now that you’ve become familiar with the basics of debugging JavaScript in VS Code, we’re going to see how to debug a slightly more complex project using the Debugger for Chrome extension. Simply open the marketplace panel via the action bar. Search for the extension and install it.

Debugger for Chrome

After installation, hit reload to activate the extension. Let’s quickly review the code that we’ll be debugging. The web application is mostly a client-side JavaScript project that’s launched by running an Express server:

const express = require('express');

const app = express();
const port = 3000;

// Set public folder as root
app.use(express.static('public'));

// Provide access to node_modules folder
app.use('/scripts', express.static(`${__dirname}/node_modules/`));

// Redirect all traffic to index.html
app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`));

app.listen(port, () => {
  console.info('listening on %d', port);
});

All the client-side code is in the public folder. The project’s dependencies include Semantic-UI-CSS, jQuery, Vanilla Router, Axios and Handlebars. This is what the project looks like when you run it with npm start. You’ll have to open the URL localhost:3000 in your browser to view the application.

Places missing table

Try to add a new place. When you do, you’ll see that nothing seems to be happening. Clearly something’s going wrong, so it’s time to look under the hood. We’ll first review the code before we start our debugging session. Open public/index.html. Our focus currently is this section:

<!-- TEMPLATES -->
<!-- Places Form Template -->
<script id="places-form-template" type="text/x-handlebars-template">
  <h1 class="ui header">
    <i class="map marker alternate icon"></i>
    <div class="content"> Places</div>
  </h1>
  <hr>
  <br>
  <form class="ui form">
    <div class="fields">
      <div class="inline field">
        <label>City</label>
        <input type="text" placeholder="Enter city" id="city" name="city">
      </div>
      <div class="inline field">
        <label>Country</label>
        <input type="text" placeholder="Enter Country" name="country">
      </div>
      <div class="ui submit primary button">Add Place</div>
    </div>
  </form>
  <br>
  <div id="places-table"></div>
</script>

<!-- Places Table Template -->
<script id="places-table-template" type="text/x-handlebars-template">
  <table class="ui celled striped table">
    <thead>
      <tr>
        <th>Id</th>
        <th>City</th>
        <th>Country</th>
        <th>NumType</th>
      </tr>
    </thead>
    <tbody>
      {{#each places}}
      <tr>
        <td>{{id}}</td>
        <td>{{city}}</td>
        <td>{{country}}</td>
        <td>{{numType}}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>
</script>

If you take a quick glance, the code will appear to be correct. So the problem must be in app.js. Open the file and analyze the code there. Below are the sections of code you should pay attention to. Take your time to read the comments in order to understand the code.

// Load DOM roots
const el = $('#app');
const placesTable = $('#places-table');

// Initialize empty places array
const places = [];

// Compile Templates
const placesFormTemplate = Handlebars.compile($('#places-form-template').html());
const placesTableTemplate = Handlebars.compile($('#places-table-template').html());

const addPlace = (city, country) => {
  const id = places.length + 1;
  const numType = (id % 2 === 0) ? 'even' : 'odd';
  places.push({
    id, city, country, numType,
  });
};

// Populate places array
addPlace('Nairobi', 'Kenya');

...

// Places View - '/'
router.add('/', () => {
  // Display Places Form
  const html = placesFormTemplate();
  el.html(html);
  // Form Validation Rules
  $('.ui.form').form({
    fields: {
      city: 'empty',
      country: 'empty',
    },
  });
  // Display Places Table
  const tableHtml = placesTableTemplate({ places });
  placesTable.html(tableHtml);
  $('.submit').on('click', () => {
    const city = $('#city').val();
    const country = $('#country').val();
    addPlace(city, country);
    placesTable.html(placesTableTemplate({ places }));
    $('form').form('clear');
    return false;
  });
});

Everything seems fine. But what could be the problem? Let’s place a breakpoint on line 53 where it says:

placesTable.html(tableHtml);

Next, create a Chrome configuration via the debug panel. Select the highlighted option:

Chrome debug config

Then update the Chrome config as follows to match our environment:

{
  "type": "chrome",
  "request": "launch",
  "name": "Launch Chrome",
  "url": "http://localhost:3000",
  "webRoot": "${workspaceFolder}/public"
},

Next, start the server as normal using npm start or node server. Then select Launch Chrome and start the debugging session. A new instance of Chrome will be launched in debug mode and execution should pause where you set the breakpoint. Now’s a good time to position Visual Studio Code and the Chrome instance side by side so you can work efficiently.

Debugging JavaScript with Chrome and VS Code

Mouse over the placesTable constant. A popup appears, but it seems empty. In the watch panel, add the expressions el and placesTable. Or, alternatively, just scroll up to where the constants have been declared.

Element popup

Notice that el is populated but placesTable is empty. This means that jQuery was unable to find the element #places-table. Let’s go back to public/index.html and find where this #places-table is located.

Aha! The table div we want is located on line 55, right inside the places-form-template. This means the div #places-table can only be found after the template, places-form-template, has been loaded. To fix this, just go back to app.js and move the code to line 52, right after the “Display Places Table” comment:

const placesTable = $('#places-table');

Save the file, and restart the debugging session. When it reaches the breakpoint, just hit Continue and let the code finish executing. The table should now be visible:

Chrome fixed table

You can now remove the breakpoint. Let’s try adding a new place — for example, Cape Town, South Africa

Country missing

Hmm … that’s not right. The place is added, but the country is not being displayed. The problem obviously isn’t the HTML table code, since the first row has the country cell populated, so something must be happening on the JavaScript side. Open app.js and add a breakpoint on line 58 where it says:

addPlace(city, country);

Restart the debug session and try to add a new place again. The execution should pause at the breakpoint you just set. Start hovering over the relevant variables. You can also add expressions to the watch panel, as seen below:

Country bug found

As you can see, the country variable is undefined, but the city variable is. If you look at the jQuery selector expressions that have been set up in the watch panel, you’ll notice that the #country selector returns nothing. This means it wasn’t present in the DOM. Head over to index.html to verify.

Alas! If you look at line 59 where the country input has been defined, it’s missing the ID attribute. You need to add one like this:

<input type="text" placeholder="Enter Country" name="country" id="country">

Restart the debugging session and try to add a new place.

Country bug fixed

It now works! Great job fixing another bug without console.log. Let’s now move on to our final bug.

Debugging Client-side Routing

Click the Convert link in the navigation bar. You should be taken to this view to perform a quick conversion:

Convert currency

That runs fine. No bugs there.

Actually there are, and they have nothing to do with the form. To spot them, refresh the page.

As soon as you hit reload, the user is navigated to back to /, the root of the app. This is clearly a routing problem which the Vanilla Router package is suppose to handle. Head back to app.js and look for this line:

router.navigateTo(window.location.path);

This piece of code is supposed to route users to the correct page based on the URL provided. But why isn’t it working? Let’s add a breakpoint here, then navigate back to the /convert URL and try refreshing the page again.

As soon as you refresh, the editor pauses at the breakpoint. Hover over the express windows.location.path. A popup appears which says the value is undefined. Let’s go to the debug console and start typing the expression below:

Navigation error

Hold up! The debug console just gave us the correct expression. It’s supposed to read window.location.pathname. Correct the line of code, remove the breakpoint and restart the debugging session.

Navigate to the /convert URL and refresh. The page should reload the correct path. Awesome!

That’s the last bug we’re going to squash, but I do recommend you keep on experimenting within the debug session. Set up new breakpoints in order to inspect other variables. For example, check out the response object in the router('/convert') function. This demonstrates how you can use a debug session to figure out the data structure returned by an API request when dealing with new REST endpoints.

Response inspect

Summary

Now that we’ve come to the end of this tutorial, you should be proud of yourself for learning a vital skill in programming. Learning how to debug code properly will help you fix errors faster. You should be aware, however, that this article only scratches the surface of what’s possible, and you should take a look at the complete debugging documentation for VS Code. Here you’ll find more details about specific commands and also types of breakpoint we haven’t covered, such as Conditional Breakpoints.

I hope from now on you’ll stop using console.log to debug and instead reach for VS Code to start debugging JavaScript like a pro!

Sponsors