Interactive Debugging with Node.js

A “step-through debugger” (also called an “interactive debugger” or just “debugger”) is a powerful tool that can be very handy when your application isn’t behaving the way you expect. You can use it to pause your application’s code execution to:

Inspect or alter application state.

See the code execution path (“call stack”) that leads to the currently-executing line of code.

Inspect the application state at earlier points on the code execution path.

Interactive debuggers* are familiar to every Java developer (among others), but they are less well known in the JavaScript world. This is unfortunate, both because debuggers can help diagnose logic issues, and because the JavaScript debugging tools today are the best and easiest to use they’ve ever been! This post will introduce the Node.js debugging tools in Visual Studio Code (VS Code) for programmers who have never used a debugger before.

Why Use a Debugger?

Debuggers are useful when writing your own code, but they really show their value when you’re working with an unfamiliar codebase. Being able to step through the code execution line by line and function by function can save hours of poring over source code, trying to step through it in your head.

The ability to change the value of a variable at runtime enables you to play through different scenarios without hard coding values or restarting your application. Conditional breakpoints let you halt execution upon encountering an error to figure out how you got there. Even if using a debugger isn’t part of your everyday process, knowing how they work adds a powerful tool to your toolbox!

Setting Up

You’ll use VS Code, which has a built-in Node.js debugger. To run code in the context of the VS Code debugger, you must first make VS Code aware of our Node.js project. For this demo you’ll create a simple Express app with express-generator, as follows:

With VS Code open, open the Debug view by clicking the bug icon in left sidebar menu. With the Debug view open, the gear icon at the top of the pane will have a red dot over it. This is because there are currently no “launch configurations:” configuration objects that tell VS Code how to run your application. Click the gear icon, select “Node.js,” and VS Code will generate a boilerplate launch configuration for you.

There are two ways to attach the VS Code debugger to your application:

Set up a launch configuration and launch your app from within VS Code

Start your app from the console with node --debug-brk your-app.js and run the “Attach” launch configuration

The first approach normally requires some setup (beyond the scope of this post, but read about it here), but because the package.json has a run script, VS Code automagically created a launch configuration based on that script. That means you can simply click the green “run” button to start our application in the debugger.

If all went well, you will see a new toolbar at the top of your screen with pause and play buttons (among others). The Debug Console at the bottom of the screen will tell you the command it ran and its output–something like this:

node--debug-brk=18764--nolazybin/wwwDebuggerlisteningonport18764

When you load http://localhost:3000 in a browser, you will see the Express log messages in this same Debug Console:

GET/304327.916ms--GET/stylesheets/style.css3041.313ms--

Now that you have the code running with the VS Code debugger attached, let’s set some breakpoints and start stepping through our code….

Breakpoints

A breakpoint is a marker on a line of code that tells the debugger “pause execution here.” To set a breakpoint in VS Code, click the gutter just to the left of the line number. For example, open routes/index.js and set a breakpoint in the root request listener:

The breakpoints pane at the bottom left has a listing for the new breakpoint (along with entries for exceptions, which we’ll talk about momentarily). Now, when you hit http://localhost:3000 in a browser again, VS Code will pause at this point so you can examine what’s going on a that point:

With the code paused here, you can examine variables and their values in the variables pane and see how you got to this point in the code in the call stack pane. You may also notice that the browser has not loaded the page– that’s because it’s still waiting for the server to respond! We’ll take a look at each of the sidebar panes in turn, but for now, press the play button to continue code execution.

Now the server will send the finished page to the browser. When code execution resumes, the “play” button is no longer enabled.

Other types of breakpoints

In addition to breaking on a certain line each time it’s executed, you can add dynamic breakpoints that pause execution only in certain circumstances. Here’s a few of the more useful ones:

Conditional Breakpoint: After setting a breakpoint, right click on it and select “edit breakpoint,” then enter an expression to conditionally activate a breakpoint. For example, to activate a breakpoint only if the user is an admin, you might add user.role === "admin" to your conditional breakpoint.

Uncaught Exception: This is enabled by default. With this enabled, you don’t have to set any breakpoints to locate errors, the debugger will pause on any (uncaught) exceptions.

All Exceptions: If you have robust error handling in your application, but you still want to see where errors come from before they’re caught and handled, enable this setting. Be warned, however, that many libraries throw and catch errors internally in the normal course of their execution, so this can be pretty noisy.

Variable pane

In this pane, you can examine and change variables in the running application. Let’s edit our homepage route in routes/index.js to make the title a variable:

/* GET home page. */varourTitle='Express';router.get('/',function(req,res,next){res.render('index',{title:ourTitle});});

After editing the code, restart the debugger so it picks up the new code. Do this by clicking the green circle/arrow button in the top toolbar. After editing a file with a breakpoint already set and restarting the debugger (as you just did), you’ll also want to check that your breakpoints are still in the right spot. VS Code does a pretty good job of keeping the breakpoint on the line you expect but it’s not perfect.

With our breakpoint on what’s now line 7 and with the debugger restarted, refresh your browser. The debugger should stop on line seven. You don’t see ourTitle in the variable pane right away, because it’s not “Local” to that function, but expand the “Closure” section just below the “Local” section and there it is!

Double-clicking ourTitle in the Variables Pane allows you to edit it. This is a great way to tinker with your application and see what happens if you switch a flag from true to false, change a user’s role, or do something else– all without having to alter the actual application code or restart your application!

The variable pane is also a great way to poke around and see what’s available in objects created by libraries or other code. For example, under “Local” you can see the req object, see that its type is IncomingMessage, and by expanding it you can see the originalUrl, headers, and various other properties and methods.

Stepping

Sometimes, rather than just pausing the application, examining or altering a value, and setting it running again, you want to see what’s happening in your code line by line: what function is calling which and and how that’s changing the application state. This is where the “Debug Actions” toolbar comes in: it’s the bar at the top of the screen with the playback buttons. We’ve used the continue (green arrow) and restart (green circle arrow) buttons so far, and you can hover over the others to see the names and associated keyboard shortcuts for each. The buttons are, from left to right:

Continue/Pause: Resume execution (when paused) or pause execution.

Step over: Execute the current line and move to the next line. Use this button to step through a file line by line.

Step in: When paused on a function call, you can use this button to step into that function. This can get a bit confusing if there are multiple function calls on one line, so just play around with it.

Step out: Run the current function to its return statement & step out to the line of code that invoked that function.

Restart: Stop your debugging session (kill your application) and start it again from the beginning. Use this after altering code.

Stop: Kill your application.

Watch Expressions

While stepping through your code, you may want to see the values of certain things. A “watch expression” will run (in the current scope!) at each paused/stopped position in your code and display the expression’s value. Hover over the Watch Expression pane and click the plus to add an expression. To see the user agent header of each request as well as ourTitle, whether the response object has had headers sent, and the value of 1 + 1, just for good measure, so add the following watch expressions:

req.headers['user-agent']ourTitleres._headerSent1+1

When you refresh the browser the debugger pauses once again at the breakpoint on line 7, you can see the result of each expression:

Call Stack

The Call Stack Pane shows the function calls that got you to the current position in the code when execution is paused, and enables you to step back up that stack and examine the application state in earlier “frames.” By clicking the frame below the current frame you can jump to the code that called the current function. In our case, the current frame is labeled (anonymous function) in index.js [7], and the one before that the handle function in layer.js, which is a component of the Express framework:

Stepping down into the Express framework is not something I do every day, but when you absolutely need to understand how you got to where you are, the Call Stack Pane is very useful!

One especially interesting use of the Call Stack Pane is to examine variables at earlier points in your code’s execution. By clicking up through the stack, you can see what variables those earlier functions had in their scope, as well as see the state of any global variables at that point in execution.

All This and More…

There are many more features of the interactive debugger than I went over here, but this is enough to get you started. To learn more, take a look at the excellent documentation from Microsoft on the VS Code Debugger and using it with Node.js. Oh, and I should probably mention that all the debugging features outlined here (and more) are built-in to Firefox as well as Chrome, should you wish to use them on browser-based code. Happy Debugging!

There’s no specific term I’ve found for this common collection of application debugging tools so I’m using the term “interactive debugging” in this article.