Debugging is an integral part of the software development process. Understanding its strategies and complexities is not only essential but also shapes the core of all development efforts. In the context of offering Node JS development services, there are multiple debugging strategies available while building NodeJs applications.
In this tutorial, we will discuss these methodologies and explore how to debug NodeJs applications using tools like the terminal, NodeJs’ built-in debugger statement, Chrome Dev Tools, 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 some source (we will be using JSON placeholder for that) and manipulates this set of data before saving it in a JSON file in the application folder. Now, let’s start by building our application as decoupled as possible for our own convenience.
Building the Application
We will create a new folder with the name nodejs-debugging and then enter npm init -y command inside of that folder to create a package.json file. Then, we will install Express, nodemon, axios, and cors packages by running npm i express axis cors nodemon. ExpressJS is a minimalistic NodeJS framework, axios will help us fetching data properly, cors will ensure that we won’t encounters cors errors, and nodemon will watch the server as we make changes with it.
Considering that the operating system we are using is Linux, we will also enter the following commands to create some folders and an index.js :mkdir controllers data routes and touch index.js. Before starting our code, we will go into package.json file and change it as such:
{ "name": "testing-tips-nodejs", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js", "dev": "nodemon index.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "axios": "^1.4.0", "cors": "^2.8.5", "express": "^4.18.2" } }
As you can see, in the scripts we have start and dev commands that run the node process and nodemon respectively. When in development mode, we want to use nodemon for our own sake. Now, we will write a basic express server in the index.js file.
const express = require("express"); const app = express(); const cors = require("cors"); const routes = require("./routes/posts.js"); const port = process.env.PORT || 5000; app.use(cors()); app.get("/", (req, res) => { res.send("Hello World!"); }); const value = 5 - 3; app.use("/posts", routes); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); });
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. The reason why we are doing it this way is to modularize our code as much as possible so that when we want to know what is where and why, we will encounter a properly 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:
const axios = require("axios"); const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const getPosts = async (req, res) => { try { const response = await axios.get( "https://jsonplaceholder.typicode.com/posts", { params: { _limit: 15 } } ); const dataFolder = path.join(__dirname, "../data"); const dataFile = "posts.json"; if (!fs.existsSync(dataFolder)) { fs.mkdirSync(dataFolder); } const postData = response.data.map((post) => { const rating = crypto.randomInt(1, 11); // Generate a random integer between 1 and 10, inclusive return { ...post, rating, }; }); fs.writeFileSync(path.join(dataFolder, dataFile), JSON.stringify(postData)); res.status(200).json(postData); } catch (error) { res.status(404).json({ message: error.message, }); } }; module.exports = { getPosts, };
Let’s go over what’s happening in this code.
We start by importing the necessary modules like axios for fetching, fs for file system, path for path, and crypto for creating a random number. Then, with our getPosts async function, we are sending a get request to “https://jsonplaceholder.typicode.com/posts” and limiting the amount of objects we will receive by 15. This jsonplaceholder is a dummy api that is quite handy in development.
Following that, we are specifying where we will be creating a posts.json file (data folder) and confirming that we will create the folder if it does not already exist so that we won’t encounter an error due to its absence. Then, for each item that we have, we are creating 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 then handle the error via the catch statement.
Lastly, we export the getPosts function so that we can use it in the routes.
If everything goes accordingly, we should have a data/posts.json file with 15 items inside of it, that looks something like:
{ "userId": 1, "id": 10, "title": "optio molestias id quia eum", "body": "quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error", "rating": 6 }, { "userId": 2, "id": 11, "title": "et ea vero quia laudantium autem", "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", "rating": 5 }, { "userId": 2, "id": 12, "title": "in quibusdam tempore odit est dolorem", "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", "rating": 4 },
Here, userId, id, title, and body fields are returned from the API, and we added the rating field with a random number.
Now, since we’ve written our controllers, it is time to write the routes for it so that we can import it in index.js and call it 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:
const express = require("express"); const router = express.Router(); const { getPosts } = require("../controllers/controllers.js"); router.get("/", getPosts); module.exports = router;
Here, by importing Express and using its router, we are creating a base route for the getPosts function we’re getting from the 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.
Debugging via Terminal and Watchers
Now that we have a working project ready, we can start playing around with NodeJs’ debugging functionality. The first option we have is to run node inspect index.js[or the file we want to inspect], and we should be prompted with this message on the terminal:
< Debugger listening on ws://127.0.0.1:9229/0d56efaa-fd7f-4993-be76-0437122ae1cf < For help, see: https://nodejs.org/en/docs/inspector < connecting to 127.0.0.1:9229 ... ok < Debugger attached. < Break on start in index.js:1 > 1 const express = require("express"); 2 const app = express(); 3 const cors = require("cors"); debug>
Now, we’re in the realm of debugging. We can 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 once more time, we would see something like this on the terminal :
18 app.listen(port, () => { debug> watch('value') debug> n break in index.js:18 Watchers: 0: value = 2 16 app.use("/posts", routes);
As you can see, since we’ve specified that we want to watch the value constant, the JS debugger shows us the result of the operation, and that is the number 2. This approach is similar to adding console.log statements in some sense but 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.
Debugging using the debugger keyword
Here, we change the index.js file by adding the debugger keyword after the value constant :
const express = require("express"); const app = express(); const cors = require("cors"); const routes = require("./routes/posts.js"); const port = process.env.PORT || 5000; app.use(cors()); app.get("/", (req, res) => { res.send("Hello World!"); }); const value = 5 - 3; //added debugger keyword after the value constant debugger; app.use("/posts", routes); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); });
When we rerun node inspect index.js, now, instead of manually pressing the n key over and over again, we can press c, and the debugger will directly jump to where the debugger keyword has been declared. We can therefore add our watchers as we wish like before. Here is an excerpt of the full workflow described:
sirius@sirius-20t8001ttx ~/c/a/testing-tips-nodejs [SIGINT]> node inspect index.js < Debugger listening on ws://127.0.0.1:9229/03f908f6-b6ce-4337-87b4-ed7c6eb2a027 < < For help, see: https://nodejs.org/en/docs/inspector < ok < Debugger attached. < Break on start in index.js:1 > 1 const express = require("express"); 2 const app = express(); 3 const cors = require("cors"); //pressing c here debug> c //jumps directly to line 15 where the debugger keyword has been declared break in index.js:15 13 14 const value = 5 - 3; >15 debugger; 16 17 app.use("/posts", routes); //adding watcher to value constant debug> watch('value') //going to the next line debug> n break in index.js:17 //we can see our watchers Watchers: 0: value = 2 15 debugger; 16 >17 app.use("/posts", routes); 18 19 app.listen(port, () => { debug>
Chrome DevTools: The Node JS Debugger
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’ve written -11 instead of 1, the ratings will have negative numbers as well, and we do not want that. We’ve run our application, sent the get request, and realized that the ratings include negative numbers. While this is a pretty obvious case, imagine that we are dealing with a huge function that calls other functions that call some other functions, and we need to find where the problem arises. If we’re using Chrome or Chromium-based browsers, we have Chrome Dev Tools at our disposal to check the state of the application at any given 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 ontrollers.js like so:
const postData = response.data.map((post) => { const rating = crypto.randomInt(-11, 11); // Generate a random integer between 1 and 10, inclusive console.log(rating); debugger; return { ...post, rating, }; });
Then, we rerun our application with a slightly different command => node –inspect index.js. Now, with the included lines before the inspect keyword, we have access to the server via our browser. Let’s go to the following link => chrome://inspect/#devices . Here, we should see something like this:
Here, we will click “Open dedicated DevTools for Node,” which will open something like this:
There are many things going on here. As you can see, on the left, we can change the directories and files as we want and inspect the code. On the right, we can define watchers, the keywords that the debugger will look for. We enter “rating” there, because we want to see the value of ratings. If we hadn’t included the debugger keyword on the controllers.js file too, we wouldn’t be able to see the value of ratings. Now, if we opened Postman and resent a get request to /posts, we should be prompted with a screen like this one:
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 to see 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 so:
Also, if we check what is going on with Postman, we should see that the execution is still going on because the application stops at the debugger keyword. Now, we can also do the same thing directly in VS Code.
Debugging with Visual Studio Code
Now it is time to debug code directly in VS Code. First thing we need to do is to close up our inspection server, and open VS Code. There, on the left side, there should be a Run and Debug section. If we click that, it will prompt us with a screen like this one:
Here, we will choose “create a launch.json file” and then, on the command line, if will open, we will choose “NodeJs,” and it will create a launch.json file for us that looks something like this:
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "<node_internals>/**" ], "program": "${workspaceFolder}/index.js" } ] }
Now, if we clicked launch on 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 controllers.js file:
Here too, just like Chrome Dev Tools, each refresh would result with a different rating value. Using this tool and technique, we can again look and see that there is something wrong with the ratings, and then we should go and check what’s causing the problem.
Conclusion
Throughout this tutorial, we’ve explored various strategies for debugging NodeJS applications, highlighting the importance of methods beyond simple console.log statements. As a means to ensure a smoother development process, it might be advantageous to outsource NodeJS development. By doing so, you could leverage the skills and experience of experts in the field, thereby enhancing the overall quality and efficiency of your project.
If you enjoyed this article, check out our other guides below;
- Change Node Version: A Step-by-Step Guide
- Caching Node JS: Boosting Performance & Efficiency
- Unlock the Power of Node.JS Microservices
- Unlocking the Power of Nodejs Websocket
- Best Node JS IDE & Text Editors for Application Development
FAQ
What are some of the best practices to follow while debugging in Node.js?
During a debugging session in Node.js, some of the best practices to follow include gaining a comprehensive understanding of the built-in debugger and effectively using it. It’s also beneficial to incorporate external tools such as Chrome DevTools and VS Code into your debug configuration. Rather than relying heavily on “console.log”, consider replacing it with more robust logging and debugging tools wherever practical. It’s also crucial to get acquainted with ‘inspect’ and ‘inspect-brk’ options available in Node.js, which allow you to halt your javascript code execution and step through it methodically. Incorporating linters in your debug configuration is also recommended as they help in identifying common coding errors early on.
Lastly, the practice of writing unit tests remains invaluable; not only does it assist in pinpointing bugs, but it also works preemptively to prevent potential future issues.
How can I utilize built-in debugging tools in Node.js to enhance my debugging process?
Node.js is equipped with a robust built-in debugging tool that can notably amplify your debugging process. To utilize this tool, initiate the debugger by executing your application with the ‘inspect’ or ‘inspect-brk’ command-line options, succeeded by the path to your script. For instance, using ‘node inspect app’ would set your application to debug mode. The ‘inspect-brk’ option, on the other hand, allows your script to commence but pauses execution until a debugging client is connected, giving you the capacity to debug the initialization process of your script.
Once the debugging client is operational, it allows you to meticulously step through your code, investigate variables, and appraise expressions. By incorporating the ‘debugger’ keyword in your code, you can set breakpoints that pause the execution of your script, allowing you to examine the program state in detail. This can be conveniently monitored in the debug console. If you prefer a graphical interface for a more intuitive experience, both Chrome DevTools debugger and VS Code provide viable solutions. Their debug panel can connect to Node’s debugging session, ensuring a user-friendly debugging process that visually represents your code’s state and execution flow.
Can you give an example of an advanced Node.js debugging technique and how to use it effectively?
An advanced Node.js debugging technique involves the use of the ‘post-mortem’ debugging process. In this method, a ‘core dump’, or a detailed snapshot of the application state, is created when the application encounters a crash or another critical event. Analyzing these core dumps, which can be achieved using tools such as ‘llnode’ and ‘mdb_v8’, offers deep insights into what led to the issue. Generating a core dump can be initiated from the debug prompt by using built-in Node.js options or external modules like ‘node-report’ when running your application, for example, ‘node app’.
When your application crashes or upon manual triggering, ‘node-report’ will generate a diagnostics summary file that is easy to comprehend. This file presents vital information about the JavaScript stack, native stack, heap statistics, system information, and resource usage, which can prove indispensable when debugging issues that are challenging to reproduce.
While this advanced technique necessitates a profound understanding of the language and runtime, its effective usage can expose the underlying cause of complicated bugs, particularly those concerning performance and system crashes.