BairesDev

Node JS Debugging: Tips & Techniques for Faster Troubleshooting

Node.js debugging goes beyond console.log. See how to use the built-in inspector, Chrome DevTools, and VS Code to find bugs faster.

Last Updated: September 9th 2026
Software Development
14 min read
Verified Top Talent Badge
Verified Top Talent
Monica Dodla
By Monica Dodla
Data Analytics Engineer10 years of experience

Monica is a Data Analytics Engineer at BairesDev, where she has worked in data roles for over four years. Her background includes data analysis work at Tata Consultancy Services.

Debugging is an integral part of the software development process. The faster you can pinpoint why code isn’t behaving as expected, the less time you lose to trial-and-error fixes and repeated deploys. When offering Node.js development services, you can use multiple debugging strategies to build Node.js applications.

In this tutorial, we will cover basic debugging methodologies and explore how to use the node debugger via the terminal, Node.js’ built-in debugger statement, Chrome DevTools, and Visual Studio Code.

We will start by creating a basic application with the following scenario: Our task is to build an application that fetches data from a source (we will use JSONPlaceholder for that) and manipulates the data before saving it as a JSON file in the application folder. Now, let’s start by building our application as decoupled as possible for our own convenience.

Key points

  • Node.js includes a built-in debugger: use node inspect file.js for the terminal CLI or node –inspect file.js to attach Chrome DevTools or VS Code on the default port (9229).
  • The legacy –debug, –debug-brk, and node debug commands are obsolete. Modern debugging relies on node inspect, –inspect, and –inspect-brk.
  • A single debugger; statement lets you jump directly to the code you want to inspect instead of stepping through every line manually.

Building the Application

We will create a new folder named nodejs-debugging, then run npm init -y inside that folder to create a package.json file. Then, we will install Express, nodemon, axios, and cors packages by running npm i express axios cors nodemon. Express.js is a minimalistic Node.js framework; axios will help us fetch data; CORS will prevent CORS errors; and nodemon will watch the server as we make changes.

Since we are using Linux, we will also run the following commands to create folders and an index.js: mkdir controllers data routes and touch index.js. Before starting our code, we will go into the package.json file and change it as follows:

Code
1{
2  "name": "testing-tips-nodejs",
3  "version": "1.0.0",
4  "description": "",
5  "main": "index.js",
6  "scripts": {
7    "test": "echo \"Error: no test specified\" && exit 1",
8    "start": "node index.js",
9    "dev": "nodemon index.js"
10  },
11  "keywords": [],
12  "author": "",
13  "license": "ISC",
14  "dependencies": {
15    "axios": "^1.4.0",
16    "cors": "^2.8.5",
17    "express": "^4.18.2"
18  }
19}

As you can see, the scripts include the start and dev commands that run the Node process and nodemon, respectively. When in development mode, we want to use nodemon for convenience. Now, we will write a basic Express server in the index.js file.

Code
1const express = require("express");
2const app = express();
3const cors = require("cors");
4const routes = require("./routes/posts.js");
5
6const port = process.env.PORT || 5000;
7
8app.use(cors());
9
10app.get("/", (req, res) => {
11  res.send("Hello World!");
12});
13
14const value = 5 - 3;
15
16app.use("/posts", routes);
17
18app.listen(port, () => {
19  console.log(`Example app listening at http://localhost:${port}`);
20});

You will see that we have included some extra lines like routes, posts, and a value. Our server won’t work right now if we run npm run dev because we do not have those routes yet. We’re doing it this way to modularize our code as much as possible, so when we want to know what is where and why, we will find a well-managed folder structure instead of a large index.js file.

Writing the Controllers

Now, in our controllers directory, we will create a controllers.js file and add the following snippet inside of it:

Code
1const axios = require("axios");
2const fs = require("fs");
3const path = require("path");
4const crypto = require("crypto");
5
6const getPosts = async (req, res) => {
7  try {
8    const response = await axios.get(
9      "https://jsonplaceholder.typicode.com/posts",
10      { params: { _limit: 15 } }
11    );
12
13    const dataFolder = path.join(__dirname, "../data");
14    const dataFile = "posts.json";
15
16    if (!fs.existsSync(dataFolder)) {
17
18      fs.mkdirSync(dataFolder);
19    }
20
21    const postData = response.data.map((post) => {
22      const rating = crypto.randomInt(1, 11); // Generate a random integer between 1 and 10, inclusive
23
24      return {
25        ...post,
26        rating,
27      };
28    });
29
30    fs.writeFileSync(path.join(dataFolder, dataFile), JSON.stringify(postData));
31
32    res.status(200).json(postData);
33  } catch (error) {
34    res.status(404).json({
35      message: error.message,
36    });
37  }
38};
39
40module.exports = {
41  getPosts,
42};

Let’s go over what’s happening in this code.

We start by importing the necessary modules like axios for fetching, fs for the file system, path for the path, and crypto for generating a random number. Then, with our getPosts async function, we send a GET request to “https://jsonplaceholder.typicode.com/posts” and limit the number of objects we will receive to 15. This jsonplaceholder is a dummy API that is quite handy in development.

Next, we specify where to create a posts.json file (data folder) and confirm that we will create the folder if it doesn’t already exist, so we don’t encounter an error due to its absence. Then, for each item, we create a random number between 1 and 10 inclusive. Afterward, using the spread operator, we are adding this new randomized rating key/value pair to the data we already have. To close, we tie everything together and handle errors with the catch statement.

Lastly, we export the getPosts function so that we can use it in the routes.

If everything goes according to plan, we should have a data/posts.json file with 15 items inside of it that looks something like:

Code
1{
2"userId": 1,
3"id": 10,
4"title": "optio molestias id quia eum",
5"body": "quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error",
6"rating": 6
7  },
8  {
9"userId": 2,
10"id": 11,
11"title": "et ea vero quia laudantium autem",
12"body": "delectus reiciendis molestiae occaecati non minima eveniet qui voluptatibus\naccusamus in eum beatae sit\nvel qui neque voluptates ut commodi qui incidunt\nut animi commodi",
13"rating": 5
14  },
15  {
16"userId": 2,
17"id": 12,
18"title": "in quibusdam tempore odit est dolorem",
19"body": "itaque id aut magnam\npraesentium quia et ea odit et ea voluptas et\nsapiente quia nihil amet occaecati quia id voluptatem\nincidunt ea est distinctio odio",
20"rating": 4
21  },

Here, the API returns the userId, id, title, and body fields, and we added the rating field with a random number.

Now that we’ve written our controllers, it is time to write the routes so we can import them in index.js and call them via curl or a tool like Postman.

Writing the Routes

Let’s go to the routes directory and create a posts.js file and paste the following snippet inside it:

Code
1const express = require("express");
2const router = express.Router();
3
4const { getPosts } = require("../controllers/controllers.js");
5
6router.get("/", getPosts);
7
8module.exports = router;

Here, by importing Express and using its router, we create a base route for the getPosts function we’re getting from controllers.js. We also export the router. Now, our index.js file makes sense. If we send a GET request to http://localhost:5000/posts, the data/posts.json file will be created as specified.

How Do You Start the Built-In Node.js Debugger From the Terminal?

Now that we have a working project, we can start exploring Node.js’ debugging functionality. The first option is to run node inspect index.js (or whichever file we want to inspect) to start the app in debug mode, and we should see a message like this in the terminal:

Code
1< Debugger listening on ws://127.0.0.1:9229/0d56efaa-fd7f-4993-be76-0437122ae1cf
2< For help, see: https://nodejs.org/en/docs/inspector
3< 
4connecting to 127.0.0.1:9229 ... ok
5< Debugger attached.
6< 
7Break on start in index.js:1
8> 1 const express = require("express");
9  2 const app = express();
10  3 const cors = require("cors");
11debug>

Now, we’re in the realm of debugging, where you can set breakpoints and enter certain keywords to do actions.

  • Pressing c or cont will continue the code execution to the next breakpoint or to the end.
  • Pressing n or next will move to the next line.
  • Pressing s or step will step into a function.
  • Pressing o will step out of a function
  • Writing pause will pause the running code.

If we were to press “n” a couple of times, and after we see the value constant write watch(‘value’), then press n one more time, we would see something like this on the terminal:

Code
118 app.listen(port, () => {
2debug> watch('value')
3debug> n
4break in index.js:18
5Watchers:
6  0: value = 2
7
8 16 app.use("/posts", routes);

As you can see, since we’ve specified that we want to watch the value of the constant, the JS debugger shows us the result of the operation, which is 2. Watchers like this let you track variable values in real time as your code executes.

This approach is similar to adding console.log statements in some sense, but it’s mostly useful in a very small scope. Imagine if we had to press n hundreds of times to go into a line. That wouldn’t be very productive. For that, we have another option.

How Do You Set a Breakpoint With the Debugger Keyword?

Here, we change the index.js file (or any JS file) by adding the debugger keyword after the value constant:

Code
1const express = require("express");
2const app = express();
3const cors = require("cors");
4const routes = require("./routes/posts.js");
5
6const port = process.env.PORT || 5000;
7
8app.use(cors());
9
10app.get("/", (req, res) => {
11  res.send("Hello World!");
12});
13
14const value = 5 - 3;
15//added debugger keyword after the value constant
16debugger;
17
18app.use("/posts", routes);
19
20app.listen(port, () => {
21  console.log(`Example app listening at http://localhost:${port}`);
22});

When we rerun node inspect index.js, instead of manually pressing the n key over and over again, we can press c, and the debugger will jump directly to where the debugger keyword is declared. We can therefore add our watchers as we wish, like before. Here is an excerpt of the full workflow described:

Code
1sirius@sirius-20t8001ttx ~/c/a/testing-tips-nodejs [SIGINT]> node inspect index.js
2< Debugger listening on ws://127.0.0.1:9229/03f908f6-b6ce-4337-87b4-ed7c6eb2a027
3< 
4< For help, see: https://nodejs.org/en/docs/inspector
5< 
6 ok
7< Debugger attached.
8< 
9Break on start in index.js:1
10> 1 const express = require("express");
11  2 const app = express();
12  3 const cors = require("cors");
13//pressing c here
14debug> c
15//jumps directly to line 15 where the debugger keyword has been declared
16break in index.js:15
17 13 
18 14 const value = 5 - 3;
19>15 debugger;
20 16 
21 17 app.use("/posts", routes);
22//adding watcher to value constant
23debug> watch('value')
24//going to the next line
25debug> n
26break in index.js:17
27//we can see our watchers
28Watchers:
29  0: value = 2
30
31 15 debugger;
32 16 
33>17 app.use("/posts", routes);
34 18 
35 19 app.listen(port, () => {
36debug>

How Do You Debug Node.js With Chrome DevTools?

Now, we are going to change our strategy a bit and use other debugging tools. Imagine that we’ve made a mistake in our controllers. Instead of writing const rating = crypto.randomInt(1, 11); , we wrote const rating = crypto.randomInt(-11, 11); .

Now, since we wrote -11 instead of 1, the ratings include negative numbers, and we don’t want that. We ran our application, sent the GET request, and realized the ratings include negative numbers. This is a pretty obvious case, but imagine we are dealing with a huge function that calls other functions that call other functions, and we need to find where the problem arises. If we’re using Chrome or Chromium-based browsers, we have Chrome DevTools to check the application’s state at any debug point in a way that’s easier to follow visually. To start with, let’s stop our server and change the postData function in controllers.js like so:

Code
1    const postData = response.data.map((post) => {
2      const rating = crypto.randomInt(-11, 11); // Generate a random integer between 1 and 10, inclusive
3      console.log(rating);
4      debugger;
5
6      return {
7        ...post,
8        rating,
9      };
10    });

Then, we rerun our application with a slightly different command => node –inspect index.js. Now, with the lines before the inspect keyword included, we can access the server via our browser. Let’s go to the following link => chrome://inspect/#devices . Here, we should see something like this:

Chrome DevTools Devices panel showing USB and network discovery with a Node.js remote debugging target.

Here, we will click “Open dedicated DevTools for Node,” which will open something like this:

Chrome DevTools for Node.js showing an Express application source file with a debugger statement and breakpoint controls.

Chrome DevTools for Node.js showing an Express application source file with a debugger statement and breakpoint controls.

Chrome DevTools for Node.js showing a paused debugger in an Express controller with source code, variables, scope, and call stack.

Now, you see that the value of rating is -9. We can infer that we’ve done something wrong in our code that caused this behavior, and check whether the rating constant accepts values between -11 and 11. Now, if we click F8 to resume the execution, we would see a different value like this:

Node.js DevTools paused at a debugger statement in an Express controller, showing runtime values, scope variables, and the call stack.

Also, if we check Postman, we should see that the execution is still running because the application stops at the debugger keyword. Now, we can also do the same thing directly in VS Code.

How Do You Debug Node.js Directly Inside VS Code?

Now it is time to debug code directly in VS Code. First, we need to close our inspection server and open VS Code. On the left side, you should see a Run and Debug section. If we click that, it will prompt us with a screen like this one:

Visual Studio Code showing a Node.js Express controller with the Run and Debug panel and a debugger statement in the source code.

Here, we will choose “create a launch.json file”. A command line will open, where we will choose “NodeJs,” and it will create a launch.json file for us that looks something like this:

Code
1{
2    // Use IntelliSense to learn about possible attributes.
3    // Hover to view descriptions of existing attributes.
4    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5    "version": "0.2.0",
6    "configurations": [
7        {
8            "type": "node",
9            "request": "launch",
10            "name": "Launch Program",
11            "skipFiles": [
12                "<node_internals>/**"
13            ],
14            "program": "${workspaceFolder}/index.js"
15        }
16    ]
17}

This launch configuration is stored in launch.json inside the .vscode folder at your project root. VS Code creates this folder automatically the first time you set up a debug configuration. You can also set environment variables in launch configurations, either inline using the env key or by pointing to a .env file with envFile.

Now, if we clicked Launch in the top left, or clicked F5 this time, it will start the inspection server itself. If we would send a GET request via Postman, it will send us to the debugger keyword in the controllers.js file:

Visual Studio Code debugging a Node.js Express controller, showing a paused breakpoint, local variables, call stack, and application output.

Here too, just like Chrome DevTools, each refresh would result in a different rating value. Using this tool and technique, we can see that something is wrong with the ratings, and then check what’s causing the problem.

Alternatively, VS Code offers a JavaScript Debug Terminal (accessible from the Run and Debug panel or the terminal type dropdown). Any JS process you run inside this terminal is automatically debugged, without needing a launch.json file. It’s a faster option when you just want to debug a quick script without setting up a configuration first.

VS Code also has an Auto Attach feature. When enabled, it automatically connects the debugger to Node.js processes you launch from the regular integrated terminal, so you don’t need a dedicated debug terminal or configuration file.

Comparison Table

Method How to Start Interface Best For Status
node inspect CLI node inspect app.js Terminal, keyboard commands Quick, no-GUI stepping on a server Built-in, current
Chrome DevTools node –inspect app.js, then chrome://inspect Visual browser UI Inspecting variable state and call stack visually Built-in, current
VS Code Debugger launch.json + F5 Editor-integrated UI Source-map/TypeScript debugging in your editor Built-in, current
debugger; keyword Add debugger; to code Works with any debugger Jumping directly to a breakpoint Built-in, current

When Would You Actually Use These Debugging Techniques?

  • Tracing why a mapped value returns incorrect results by placing a watcher on the variable inside a transformation function.
  • Debugging application startup with –inspect-brk so execution pauses before route handlers are registered.
  • Performing post-mortem analysis of intermittent production crashes using core dumps and llnode.

What Are Some Node.js Debugging Best Practices?

  • Build a solid understanding of the built-in debugger and use it as your primary tool.
  • Incorporate external tools such as Chrome DevTools and VS Code into your debug configuration for a visual workflow.
  • Avoid relying heavily on console.log and replace it with more robust logging and debugging tools wherever practical.
  • Get familiar with the inspect and inspect-brk options to halt and step through code execution methodically.
  • Add linters to your debug configuration to catch common coding errors early.
  • Practice writing unit tests. They help pinpoint existing bugs and catch future issues early.

A breakpoint workflow is only effective when the engineers using it understand the event loop, asynchronous execution, and the stack traces they are stepping through. That depth is what distinguishes teams that run Node.js services in production from developers who rely primarily on console.log to troubleshoot complex runtime issues.

Throughout this tutorial, we’ve explored several ways to debug Node.js applications, highlighting the importance of methods beyond simple console.log statements. If your team doesn’t have that expertise in-house, outsourcing Node.js development is one option. Working with Node.js experts who already know these tools well means fewer debugging sessions eating into your project timeline.

Frequently Asked Questions

  • Run node inspect yourfile.js for the built-in terminal debugger, or node –inspect yourfile.js to expose the inspector for Chrome DevTools and VS Code on port 9229.

  • –inspect starts the inspector while allowing the application to continue running. –inspect-brk pauses execution on the first line, making it useful for debugging application initialization.

  • No. The legacy Debugger API was deprecated in Node 7.7.0 and removed. Modern Node versions use the V8 Inspector, so you should use –inspect, –inspect-brk, and node inspect.

  • No. GoogleChromeLabs/ndb was archived in July 2023, and node-inspector/node-inspect have been deprecated with their functionality merged into Node.js core.

  • When execution reaches a debugger; statement while running under the inspector, the program pauses, allowing you to inspect variables, evaluate expressions, and step through execution.

  • Generate a core dump or a node –report diagnostics file, then analyze it using tools such as llnode or mdb_v8 to inspect the JavaScript and native stacks after the crash.

Verified Top Talent Badge
Verified Top Talent
Monica Dodla
By Monica Dodla
Data Analytics Engineer10 years of experience

Monica is a Data Analytics Engineer at BairesDev, where she has worked in data roles for over four years. Her background includes data analysis work at Tata Consultancy Services.

  1. Blog
  2. Software Development
  3. Node JS Debugging: Tips & Techniques for Faster Troubleshooting

Hiring engineers?

We provide nearshore tech talent to companies from startups to enterprises like Google and Rolls-Royce.

Alejandro D.
Alejandro D.Sr. Full-stack Dev.
Gustavo A.
Gustavo A.Sr. QA Engineer
Fiorella G.
Fiorella G.Sr. Data Scientist

BairesDev assembled a dream team for us and in just a few months our digital offering was completely transformed.

VP Product Manager
VP Product ManagerRolls-Royce

Hiring engineers?

We provide nearshore tech talent to companies from startups to enterprises like Google and Rolls-Royce.

Alejandro D.
Alejandro D.Sr. Full-stack Dev.
Gustavo A.
Gustavo A.Sr. QA Engineer
Fiorella G.
Fiorella G.Sr. Data Scientist