JavaScript

How to Debug Node.js Applications

Even when implementing smaller Node.js applications, sooner or later, you’ll reach the point at which you need to find and fix a bug in your own source code.

 

Most of these errors are caused by incorrectly assigned variables or logic errors within the Node.js application. For this reason, when troubleshooting, you’re usually concerned with the values of certain variables and the flow of the application logic.

 

In the simplest case, you insert console.log statements at certain points in the source code, which helps you output the values of the variables and check the individual flow steps of your source code. In the listing below, you can see what the web server example can look like with such debug outputs. In addition, as you can see in the code, console.log is quite flexible. You can pass either one or more arguments to this method. If you pass multiple arguments, console.log takes care of the joining and the output.

 

import { createServer } from 'http';

 

console.log('createServer');

const server = createServer((request, response) => {

   console.log('createServer callback');

 

   response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });

 

   const url = new URL(request.url, 'http://localhost:8080');

 

   console.log(url);

   console.log('Name: ', url.searchParams.get('name'));

 

   const body = `...`;

 

   response.end(body);

});

 

console.log('listen');

server.listen(8080, () => {

   console.log(

       `Server is listening to http://localhost:${server.address().port}`,

   );

});

 

When you run the source code, you’ll see that first createServer, listen, and the string Server is listening to http://localhost:8080 are output to the console. As soon as you start a query with your browser, the createServer callback string, the URL object, and the name from the query string are output.

 

This way of debugging an application has several problems at the same time. To receive output, you need to actively edit the source code and include the statements in the relevant places. This means that you’ll have to make additional adjustments to the source code for error analysis, and for this reason, you’ll have to modify it further, which makes it vulnerable for additional problems. Another difficulty of this method of debugging is that by using the console.log statements, at runtime of the application, you don’t get an image of the whole environment but only of a very specific section, which can be even further distorted by certain influences.

 

The Node.js debugger provides a remedy for these problems. The advantage of the debugger is that it’s an integral part of the Node.js platform, so you don’t need to install any other software. To start the debugger, save your source code to a file, which in this example is the server.mjs file, and run the node inspect server.mjs command in the command line. Below shows the output of the command.

 

$ node inspect server.mjs

< Debugger listening on ws://127.0.0.1:9229/63f3a320-4d08-48ab-847f-300688e54760

< For help, see: https://nodejs.org/en/docs/inspector

connecting to 127.0.0.1:9229 ... ok

< Debugger attached.

Break on start in server.mjs:1

> 1 import { createServer } from 'http';

  2

  3 const server = createServer((request, response) => {

debug>

 

If you add the inspect option when calling your application, the application will start in interactive debug mode. The first information you get is that the debugger is waiting for incoming connections via a WebSocket connection. Furthermore, the display tells you in which file and line the debugger has interrupted the execution. Finally, the first three lines of the application code are output. The statement at which the debugger stopped is highlighted in green.

 

Navigating in the Debugger

The debug> prompt signals that the debugger is now waiting for your input. You can see which commands are available to you in the table below

 

Commands for the Debugger

 

In the example shown in above, you use the n or next command, respectively, to jump to the next statement with the debugger. This is marked by the fact that the createServer function is highlighted in green in line 3.

 

Information in the Debugger

If you jump further in the debugger to the second line, you can display the value of the http constant. To do so, you must enter the repl command in the debugger. This command launches an interactive shell from which you can access the debugger environment. For example, you can then enter the string http and get the structure of this variable. The shortcut (Ctrl)+(C) enables you to switch from the interactive shell back to the debugger. After that, you can use the commands from the previous table for navigation purposes as usual.

 

If you’ve created an output in the interactive shell and then jumped back to the debug mode, you can no longer see where you are in your source code. The list command helps you to solve this problem. This function ensures that the debugger shows you the currently executed line of source code and a certain number of lines before and after this line. You can specify the number of lines you want to see as an argument. If you don’t specify a number, the value 5 is assumed. Below shows an example of how to use the list command.

 

debug> list(1)

> 1 import { createServer } from 'http';

   2

 

Another function the debugger offers you is the output of a backtrace. This is especially helpful if you’ve jumped to various subroutines using the s command and now want to find out how you got to the current location. The backtrace function, or its shorter variant bt, enables you to display the backtrace of the current execution. The next listing shows the output of the backtrace in case you’ve jumped into the callback function of the createServer function in the web server example.

 

debug> bt

#0 (anonymous) server.mjs:4:2

#1 emit node:events:394:27

#2 parserOnIncoming node:_http_server:924:11

#3 parserOnHeadersComplete node:_http_common:127:16

 

When you run through the source code of your application with the debugger, you’re usually interested in the values of certain variables. You can easily determine these values via the repl command and the interactive shell. However, in many cases, this variant isn’t very handy because you have to switch back to the shell each time and can only read the values there. Another way to find out the values of variables is to use the watch function of the debugger. In this case, you pass the name of the variable whose value you want to observe as a string to this function. If you now step through your application, you’ll see the value of this variable in each step.

 

The next listing shows how you can use the watch function. The output of the structure in this case only provides the hint that the watch expression is the createServer function. However, you can not only monitor native structures but also observe any variables during debugging. But if you set a watcher on a more extensive object with many properties, the output isn’t structured. Watchers therefore offer an advantage mainly for variables with scalar values or small objects. For more extensive objects, we recommend using the repl command.

 

debug> watch('createServer')

debug> n

break in server.mjs:3

Watchers:

   0: createServer = [Function: createServer]

 

   1 import { createServer } from 'http';

   2

>  3 const server = createServer((request, response) => {

   4 debugger;

   5 response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); debug>

 

In addition to the watch function, there are two other functions you can use for monitoring structures. The unwatch function allows you to remove watchers that have already been set. For this purpose, you only have to pass the name of the variable as a string to the function, and the watcher will be removed. The second watchers function takes no arguments and lists the existing watchers and the associated values of the corresponding variables. This table summarizes the debugger commands for you again.

 

Commands in the Debugger

 

Breakpoints

With the current state, it’s only possible for you to go through the source code of your application step by step from the beginning to the point where you suspect a problem. This can be difficult with extensive applications, as it may take a long time to get to the relevant location. For these cases, the Node.js debugger provides the breakpoints feature. A breakpoint represents a marker in the source code at which the debugger automatically stops.

 

With the setBreakpoint function (or sb in the short form) of the debugger, you can define a breakpoint. In the case of the next listing, you use setBreakpoint to set a breakpoint in line 7 at the start of the run. You can use the c command to continue execution, which then stops at the breakpoint.

 

setBreakpoint allows you to set breakpoints in several different ways. As you’ve already seen, you can specify a particular line in which you want to set the breakpoint. If you don’t specify a value, the breakpoint is set in the current line. In addition, you can specify a function as a string at whose first statement the execution will stop. Finally, in the last variant, you specify a file name and a line number. When the execution of the application reaches this file and line, the execution will be interrupted. The clearBreakpoint or cb command enables you to remove a set breakpoint by specifying the file name and the line number.

 

$ node inspect server.mjs

< Debugger listening on ws://127.0.0.1:9229/92b7f273-d2ec-4134-a700-a4b69e90a4f2

< For help, see: https://nodejs.org/en/docs/inspector

connecting to 127.0.0.1:9229 ... ok

< Debugger attached.

Break on start in server.mjs:1

>  1 import { createServer } from 'http';

   2

   3 const server = createServer((request, response) => {

debug> setBreakpoint(8)

   3 const server = createServer((request, response) => {

   4    response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });

   5

   6   const url = new URL(request.url, 'http://localhost:8080');

   7

>  8    const body = `<!DOCTYPE html>

   9        <html>

  10            <head>

  11                <meta charset="utf-8">

  12                <title>Node.js Demo</title>

  13            </head>

debug> c

< Server is listening to http://localhost:8080

break in server.mjs:8

   6    const url = new URL(request.url, 'http://localhost:8080');

   7

>  8    const body = `<!DOCTYPE html>

   9        <html>

  10            <head>

debug>

 

Alternatively, you can set a breakpoint directly in your source code by inserting the debugger statement. However, the disadvantage is that you have to modify your source code for debugging. In any case, you must make sure to remove all debugger statements after debugging. Below shows how you can use the debugger statement in the web server example.

 

import { createServer } from 'http';

 

const server = createServer((request, response) => {

   response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });

 

   const url = new URL(request.url, 'http://localhost:8080');

       debugger;

       const body = `<!DOCTYPE html>

           <html>

           <head>

               <meta charset="utf-8">

               <title>Node.js Demo</title>

           </head>

           <body>

               <h1 style="color:green">Hello ${url.searchParams.get('name')}</h1>

           </body>

       </html>`;

   response.end(body);

});

server.listen(8080, () => {

   console.log(

       `Server is listening to http://localhost:${server.address().port}`,

   );

});

 

If you start your script now, as shown below, and continue the execution using the c command, the application is ready to accept requests from clients.

 

$ node inspect server.mjs

< Debugger listening on ws://127.0.0.1:9229/5a149c3f-d4c3-4529-bc84-045a72f49357

< For help, see: https://nodejs.org/en/docs/inspector

< Debugger attached.

   OK

Break on start in server.mjs:1

>  1 import { createServer } from 'http';

   2

   3 const server = createServer((request, response) => {

debug> c

< Server is listening to http://localhost:8080

break in server.mjs:7

   5

   6 const url = new URL(request.url, 'http://localhost:8080');

>  7 debugger;

   8 const body = `<!DOCTYPE html>

   9 <html>

debug>

 

If you now access the web server via a web browser using the URL http://localhost:8080/?name=user, the execution will be interrupted within the callback function. Then the familiar features of the debugger will be available.

 

If you start your application in debug mode, it will run until the debugger hits the first breakpoint. During initial execution, you thus only have the option of setting a breakpoint via a debugger statement in the code. The --inspect-brk option provides help here by ensuring that the debugger stops at the first line; you can then connect to your developer tools, set breakpoints, and run the application.

 

Debugging with Chrome Developer Tools

The V8 inspector is integrated in the Node.js debugger and is responsible for opening a WebSocket to the outside world through which you can connect various tools to your debugging session. This makes it possible for you not only to debug on the command line but also to use graphical tools. The most convenient variant is to use Chrome in this context. The connection is established via the Chrome DevTools Protocol.

 

Instead of starting the debugger via the inspect option as before, you must use the -- inspect option for remote debugging. This listing shows the corresponding output.

 

$ node --inspect server.mjs

Debugger listening on ws://127.0.0.1:9229/53dc01a4-d65e-47a9-bb88-bc38b4e059d5

For help, see: https://nodejs.org/en/docs/inspector

Server is listening to http://localhost:8080

 

As you can see in the output, the application doesn’t stop; it just waits for incoming debugging connections as well as regular client requests. To be able to connect your browser to the running debug process, you should now enter “chrome://inspect” in the address bar of the browser. You’ll then see an overview of all available remote targets, as shown in this figure.

 

List of Debugging Sessions in Chrome

 

When you now click on the inspect link, another browser window will open, consisting only of the developer tools. These are associated with your Node.js process. You’ll now see all console messages in the Console tab of the Chrome DevTools as well. To inspect your source code, go to the Sources tab. There you can make selections from the files of your application in the left-hand pane. Clicking on one of these files displays the source code. At this point, you can set breakpoints by clicking on the corresponding line number. As soon as the execution of the application reaches this line, the process stops, and you have control over the runtime environment. This figure shows an example of such a debugging session.

 

Active Debugging Session

 

In this mode, just like on the console, you can create watch expressions, navigate through your application with the debugger, or manipulate the environment via the console. You also have access to all available variable scopes and the current backtrace. In addition to these functions, you can use the profiler to create CPU profiles and use the Memory tab to analyze the memory utilization of your application.

 

Debugging in the Development Environment

However, the actual development process takes place neither on the command line nor in the browser, but in your development environment. Most development environments used in web development, such as Visual Studio Code or the development environments of JetBrains (e.g., WebStorm), help you debug Node.js applications. The corresponding plug-ins are either included by default or can be easily installed later. For information on how to configure the debugger in your development environment, you should refer to the documentation for your development environment, which usually contains detailed step-by-step instructions.

 

If you use the debugger in your development environment, you can access the same features of the debugger as has already been possible on the command line. Thus, you can skip subroutines or dive deeper into the structures. But you also have the option to set breakpoints or to read the values of certain variables.

 

These features, combined with the capabilities of a graphical development environment, increase the level of convenience during the development and maintenance of Node.js applications.

 

Editor’s note: This post has been adapted from a section of the book Node.js: The Comprehensive Guide by Sebastian Springer.

Recommendation

Node.js: The Comprehensive Guide
Node.js: The Comprehensive Guide

If you’re developing server-side JavaScript applications, you need Node.js! Start with the basics of the Node.js environment: installation, application structure, and modules. Then follow detailed code examples to learn about web development using frameworks like Express and Nest. Learn about different approaches to asynchronous programming, including RxJS and data streams. Details on peripheral topics such as testing, security, and performance make this your all-in-one daily reference for Node.js!

Learn More
Rheinwerk Computing
by Rheinwerk Computing

Rheinwerk Computing is an imprint of Rheinwerk Publishing and publishes books by leading experts in the fields of programming, administration, security, analytics, and more.

Comments