Node.js is a JavaScript runtime based on the same V8 engine used in Google’s Chrome browser. It is often used to build cross-platform server-side and terminal applications. Node.js has become increasingly popular over the past decade because it’s easy to install, practical to use, fast, and allows client-side web developers to leverage their skills elsewhere.

However, software development remains a complex task, and your Node.js code will fail at some point. This tutorial demonstrates various tools to help debug applications and find the cause of a problem.

Let’s dive right in.

Check Out Our Video Guide to Debugging Node.js Code

Debugging Overview

“Debugging” is the name given to the various means of fixing software defects. Fixing a bug is often straightforward. Finding the cause of the bug can be considerably more complex and incur many hours of head-scratching.

The following sections describe three general types of error you will encounter.

Syntax Errors

Your code does not follow the rules of the language — for example, when you omit a closing bracket or misspell a statement such as console.lag(x).

A good code editor can help spot common problems by:

  • Color-coding valid or invalid statements
  • Type-checking variables
  • Auto-completing function and variable names
  • Highlighting matching brackets
  • Auto-indenting code blocks
  • Detecting unreachable code
  • Refactoring messy functions

Free editors such as VS Code and Atom have great support for Node.js, JavaScript, and TypeScript (which transpiles to JavaScript). Basic syntax problems can typically be spotted before you save and test your code.

A code linter like ESLint will also report syntax errors, bad indentation, and undeclared variables. ESLint is a Node.js tool you can install globally with:

npm i eslint -g

You can check JavaScript files from the command line using:

eslint mycode.js

…but it’s easier to use an editor plugin such as ESLint for VS Code or linter-eslint for Atom, which automatically validate code as you type:

ESlint in VS Code
ESlint in VS Code.

Logic Errors

Your code runs but does not work as you expect. For example, a user is not logged out when they request it; a report shows incorrect figures; data is not fully saved to a database; etc.

Logic errors can be caused by:

  • Using the wrong variable
  • Incorrect conditions, e.g. if (a > 5) rather than if (a < 5)
  • Calculations that fail to account for operator precedence, e.g. 1+2*3 results in 7 rather than 9.

Runtime (or Execution) Errors

An error only becomes evident when the application is executed, which often leads to a crash. Runtime errors could be caused by:

  • Dividing by a variable that has been set to zero
  • Attempting to access an array item that does not exist
  • Trying to write to a read-only file

Logic and runtime errors are more difficult to spot, though the following development techniques can help:

  1. Use Test-Driven Development: TTD encourages you to write tests before a function is developed, e.g. X is returned from functionY when Z is passed as a parameter. These tests are run during the initial development and subsequent updates to ensure the code continues to work as expected.
  2. Use an issue tracking system: There is nothing worse than an email claiming “Your software doesn’t work”! Issue tracking systems allow you to record specific issues, document reproduction steps, determine priorities, assign developers, and track the progress of fixes.
  3. Use source control: A source control system such as Git will help you back up code, manage revisions, and identify where a bug was introduced. Online repositories, including Github and Bitbucket, provide free space and tools for smaller or open-source projects.

You will still encounter Node.js bugs, but the following sections describe ways to locate that elusive error.

Set Appropriate Node.js Environment Variables

Environment variables set in the host operating system can control Node.js application and module settings. The most common is NODE_ENV, which usually is set to development when debugging or production when running on a live server. Set environment variables on macOS or Linux with the command:

NODE_ENV=development

or at the (classic) Windows command prompt:

set NODE_ENV=development

or Windows Powershell:

$env:NODE_ENV="development"

In the popular Express.js framework, setting NODE_ENV to development disables template file caching and outputs verbose error messages, which could be helpful when debugging. Other modules may offer similar features, and you can add a NODE_ENV condition to your applications, e.g.

// running in development mode?
const devMode = (process.env.NODE_ENV !== 'production');

if (devMode) {
  console.log('application is running in development mode');
}

You can also use Node’s util.debuglog method to conditionally output error messages, e.g.

import { debuglog } from 'util';
const myappDebug = debuglog('myapp');
myappDebug('log something');

This application will only output the log message when NODE_DEBUG is set to myapp or a wildcard such as * or my*.

Use Node.js Command Line Options

Node scripts are typically launched with node followed by the name of the entry script:

node app.js

You can also set command line options to control various runtime aspects. Useful flags for debugging include:

  • --check
    syntax check the script without executing
  • --trace-warnings
    output a stack trace when JavaScript Promises do not resolve or reject
  • --enable-source-maps
    show source maps when using a transpiler such as TypeScript
  • --throw-deprecation
    warn when deprecated Node.js features are used
  • --redirect-warnings=file
    output warnings to a file rather than stderr
  • --trace-exit
    output a stack trace when process.exit() is called.

Output Messages to the Console

Outputting a console message is one of the simplest ways to debug a Node.js application:

console.log(`someVariable: ${ someVariable }`);

Few developers realize there are many other console methods:

Console Method Description
.log(msg) standard console message
.log('%j', obj) output object as a compact JSON string
.dir(obj, opt) pretty-print object properties
.table(obj) output arrays and objects in tabular format
.error(msg) an error message
.count(label) increment a named counter and output
.countReset(label) reset a named counter
.group(label) indent a group of messages
.groupEnd(label) terminate a group
.time(label) starts a named timer
.timeLog(label) reports the elapsed time
.timeEnd(label) stops a named timer
.trace() output a stack trace (a list of all function calls made)
.clear() clear the console

console.log() also accepts a list of comma-separated values:

let x = 123;
console.log('x:', x);
// x: 123

…although ES6 destructuring offers similar output with less effort:

console.log({ x });
// { x: 123 }

The console.dir() command pretty-prints object properties in the same way as util.inspect():

console.dir(myObject, { depth: null, color: true });

Console Controversy

Some developers claim you should never use console.log() because:

  • You’re changing code and may alter something or forget to remove it, and
  • There’s no need when there are better debugging options.

Don’t believe anyone who claims they never use console.log()! Logging is quick and dirty, but everyone uses it at some point. Use whatever tool or technique you prefer. Fixing a bug is more important than the method you adopt to find it.

Use a Third-Party Logging System

Third-party logging systems provide more sophisticated features such as messaging levels, verbosity, sorting, file output, profiling, reporting, and more. Popular solutions include cabin, loglevel, morgan, pino, signale, storyboard, tracer, and winston.

Use the V8 Inspector

The V8 JavaScript engine provides a debugging client which you can use in Node.js. Start an application using node inspect, e.g.

node inspect app.js

The debugger pauses at the first line and displays a debug> prompt:

$ node inspect .\mycode.js
< Debugger listening on ws://127.0.0.1:9229/143e23fb
< For help, see: https://nodejs.org/en/docs/inspector
<
 ok
< Debugger attached.
<
Break on start in mycode.js:1
> 1 const count = 10;
  2
  3 for (i = 0; i < counter; i++) {
debug>

Enter help to view a list of commands. You can step through the application by entering:

  • cont or c: continue execution
  • next or n: run the next command
  • step or s: step into a function being called
  • out or o: step out of a function and return to the calling statement
  • pause: pause running code
  • watch(‘myvar’): watch a variable
  • setBreakPoint() or sb(): set a breakpoint
  • restart: restart the script
  • .exit or Ctrl | Cmd + D: exit the debugger

Admittedly, this debugging option is time-consuming and unwieldy. Only use it when there’s no other option, like when you’re running code on a remote server and cannot connect from elsewhere or install additional software.

Use the Chrome Browser to Debug Node.js Code

The Node.js inspect option used above starts a Web Socket server that listens on localhost port 9229. It also starts a text-based debugging client, but it’s possible to use graphical clients — such as the one built into Google Chrome and Chrome-based browsers like Chromium, Edge, Opera, Vivaldi, and Brave.

To debug a typical web application, start it with the –inspect option to enable the V8 debugger’s Web Socket server:

node --inspect index.js

Note:

  • index.js is presumed to be the application’s entry script.
  • Ensure you use --inspect with double dashes to ensure you do not start the text-based debugger client.
  • You can use nodemon instead of node if you want to auto-restart the application when a file is changed.

By default, the debugger will only accept incoming connections from the local machine. If you’re running the application on another device, virtual machine, or Docker container, use:

node --inspect=0.0.0.0:9229 index.js
node inspect
node inspect option.

You can also use --inspect-brk instead of --inspect to halt processing (set a breakpoint) on the first line so you can step through the code from the start.

Open a Chrome-based browser and enter chrome://inspect in the address bar to view local and networked devices:

Chrome inspect tool
Chrome inspect tool.

If your Node.js application does not appear as a Remote Target, either:

  • Click Open dedicated DevTools for Node and choose the address and port, or
  • Check Discover network targets, click Configure, then add the IP address and port of the device where it’s running.

Click the Target’s inspect link to launch the DevTools debugger client. This should be familiar to anyone who’s used DevTools for client-side code debugging:

Chrome DevTools
Chrome DevTools.

Switch to the Sources panel. You can open any file by hitting Cmd | Ctrl + P and entering its filename (such as index.js).

However, it’s easier to add your project folder to the workspace. This allows you to load, edit, and save files directly from DevTools (whether you think that’s a good idea is another matter!)

  1. Click + Add folder to workspace
  2. Select the location of your Node.js project
  3. Hit Agree to permit file changes

You can now load files from the left-hand directory tree:

Chrome DevTools Sources panel
Chrome DevTools Sources panel.

Click any line number to set a breakpoint denoted by a blue marker.

Debugging is based on breakpoints. These specify where the debugger should pause program execution and show the current state of the program (variables, call stack, etc.)

You can define any number of breakpoints in the user interface. Another option is to place a debugger; statement into your code, which stops when a debugger is attached.

Load and use your web application to reach the statement where a breakpoint is set. In the example here, http://localhost:3000/ is opened in any browser, and DevTools will halt execution on line 44:

Chrome breakpoint
Chrome breakpoint.

The right-hand panel shows:

  • A row of action icons (see below).
  • A Watch pane allows you to monitor variables by clicking the + icon and entering their names.
  • A Breakpoints pane shows a list of all breakpoints and allows them to be enabled or disabled.
  • A Scope pane shows the state of all local, module, and global variables. You will inspect this pane most often.
  • A Call Stack pane shows the hierarchy of functions called to reach this point.

A row of action icons is shown above Paused on breakpoint:

Chrome breakpoint icons
Chrome breakpoint icons.

From left to right, these perform the following actions:

  • resume execution: Continue processing until the next breakpoint
  • step over: Execute the next command but stay within the current code block — do not jump into any function it calls
  • step into: Execute the next command and jump into any function as necessary
  • step out: Continue processing to the end of the function and return to the calling command
  • step: Similar to step into except it will not jump into async functions
  • deactivate all breakpoints
  • pause on exceptions: Halt processing when an error occurs.

Conditional Breakpoints

Sometimes it’s necessary to wield a little more control over breakpoints. Imagine you have a loop that completed 1,000 iterations, but you’re only interested in the state of the last one:


for (let i = 0; i < 1000; i++) {
  // set breakpoint here
}

Rather than clicking resume execution 999 times, you can right-click the line, choose Add conditional breakpoint, and enter a condition such as i = 999:

Chrome conditional breakpoint
Chrome conditional breakpoint.

Chrome shows conditional breakpoints in yellow rather than blue. In this case, the breakpoint is only triggered on the last iteration of the loop.

Log Points

Log points effectively implement console.log() without any code! An expression can be output when the code executes any line, but it does not halt processing, unlike a breakpoint.

To add a log point, right-click any line, choose Add log point, and enter an expression, e.g. 'loop counter i', i:

Chrome logpoint
Chrome log point.

The DevTools console outputs loop counter i: 0 to loop counter i: 999 in the example above.

Use VS Code to Debug Node.js Applications

VS Code, or Visual Studio Code, is a free code editor from Microsoft that’s become popular with web developers. The application is available for Windows, macOS, and Linux and is developed using web technologies in the Electron framework.

VS Code supports Node.js and has a built-in debugging client. Most applications can be debugged without any configuration; the editor will automatically start the debugging server and client.

Open the starting file (such as index.js), activate the Run and Debug pane, click the Run and Debug button, and choose the Node.js environment. Click any line to activate a breakpoint shown as a red circle icon. Then, open the application in a browser as before — VS Code halts execution when the breakpoint is reached:

VS Code breakpoint
VS Code breakpoint.

The Variables, Watch, Call Stack, and Breakpoints panes are similar to those shown in Chrome DevTools. The Loaded Scripts pane shows which scripts have been loaded, although many are internal to Node.js.

The toolbar of action icons allows you to:

  • resume execution: Continue processing until the next breakpoint
  • step over: Execute the next command but stay within the current function — do not jump into any function it calls
  • step into: Execute the next command and jump into any function it calls
  • step out: Continue processing to the end of the function and return to the calling command
  • restart the application and debugger
  • stop the application and debugger

Like Chrome DevTools, you can right-click any line to add Conditional breakpoints and Log points.

For more information, refer to Debugging in Visual Studio Code.

VS Code Advanced Debugging Configuration

Further VS Code configuration may be necessary if you want to debug code on another device, a virtual machine, or need to use alternative launch options such as nodemon.

VS Code stores debugging configurations in a launch.json file inside a .vscode directory in your project. Open the Run and Debug pane, click create a launch.json file, and choose the Node.js environment to generate this file. An example configuration is provided:

VS Code debugger configuration
VS Code debugger configuration.

Any number of configuration settings can be defined as objects in the "configurations" array. Click Add Configuration… and select an appropriate option.

An individual Node.js configuration can either:

  1. Launch a process itself, or
  2. Attach to a debugging Web Socket server, perhaps running on a remote machine or Docker container.

For example, to define a nodemon configuration, select Node.js: Nodemon Setup and change the “program” entry script if necessary:

{
  // custom configuration
  "version": "0.2.0",
  "configurations": [
    {
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "name": "nodemon",
      "program": "${workspaceFolder}/index.js",
      "request": "launch",
      "restart": true,
      "runtimeExecutable": "nodemon",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "type": "pwa-node"
    }
  ]
}

Save the launch.json file and nodemon (the configuration “name”) appears in the drop-down list at the top of the Run and Debug pane. Click the green run icon to start using that configuration and launch the application using nodemon:

VS Code debugging with nodemon
VS Code debugging with nodemon.

As before, you can add breakpoints, conditional breakpoints, and log points. The main difference is that nodemon will automatically restart your server when a file is modified.

For further information, refer to VS Code Launch configurations.

The following VS Code extensions can also help you debug code hosted on remote or isolated server environments:

Other Node.js Debugging Options

The Node.js Debugging Guide provides advice for a range of text editors and IDEs, including Visual Studio, JetBrains WebStorm, Gitpod, and Eclipse. Atom offers a node-debug extension, which integrates the Chrome DevTools debugger into the editor.

Once your application is live, you could consider using commercial debugging services such as LogRocket and Sentry.io, which can record and playback client and server errors encountered by real users.

 

Summary

Historically, JavaScript debugging has been difficult, but there have been huge improvements over the past decade. The choice is as good — if not better — than those provided for other languages.

Use whatever tool is practical to locate a problem. There’s nothing wrong with console.log() for quick bug hunting, but Chrome DevTools or VS Code may be preferable for more complex issues. The tools can help you create more robust code, and you’ll spend less time fixing bugs.

What Node.js debugging practice do you swear by? Share in the comments section below!

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.