CodeTips#6: Using the Node.js debugger

The usual way to debug a Node.js application is to simply use the console module to inspect variables or objects, which is fast enough to solve small issues. We already spoke about several options on how to use the console methods in our Debugging in Frontend post, and they are all applicable on Backend Node.js as well.

But for larger applications or more complex problems, we might need to debug line by line and monitor the behavior of our code in real-time. This is where the node debugger comes to the rescue.

Node.js includes both a v8 inspector (compatible with chrome dev-tools) and a command-line debugging utility. We will start speaking about the second, which is more intuitive and does not require other tools to start debugging any application.

To start the node debugger simply run your application with node inspect.

node inspect bin/index.js

This command will do two things.

  1. Start the inspector on default port 9229
  2. Connect the command line debugger to the inspector

Instead of running the application as normal, now we landed on a debug console.

< Debugger listening on ws://127.0.0.1:9229/acd398cf-2704-44d4-9058-c5d8d4843d1e
< For help, see: https://nodejs.org/en/docs/inspector
<
connecting to 127.0.0.1:9229 ... ok
< Debugger attached.
<
Break on start in bin/index.js:7
  5  */
  6
> 7 var app = require('../app');
  8 var debug = require('debug')('express-app-example:server');
  9 var http = require('http');
debug>

Note that it breaks (or pauses) at the first line by default. It will also break in any breakpoint written in the code, identified by the debugger keyword or by breakpoints set through setBreakpoint.

break in routes/index.js:6
  4 /* GET home page. */
  5 router.get('/', function(req, res, next) {
> 6   debugger
  7
  8   res.render('index', { title: 'Express' });

Once the application is paused on a breakpoint, the real fun starts…

Step by Step Debugging

Step-by-step debugging is done by explicitly controlling the flow of control and logic of an application. To achieve that, we use some platform commands available only in the debugger.

There are some common concepts behind each of these that might be good to know, despite the difference in the words used by the Node Debugger. Here we will briefly explain both the concept and the Node.js-specific command.

  • Resume (cont, c)

    Resume the application flow, until another breakpoint is found,
    or the application ends.

  • Step Over (next, n)

    Run the next line of code and step over functions or methods.
    This command will never enter a function body.

  • Step Into (step, s)

    Enter a function called on the current line, if any, and run the first line of its body,
    or run the next line of code.

  • Step Out (out, o)

    Exits a function, if any, returning the flow to the upper scope, and running the next line after the function call.
    This command will go back to the upper scope regardless of the line where it was inside the function body.

  • Stop (kill, k)

    Stop or disconnect from the current application or process.

  • Run (run, restart, r)

    Run the application or reconnects.

  • REPL (repl)

    Enter a debug REPL (read eval print loop).
    We will address the REPL below.

For a full list of commands, type help in the debugger console.

Debug REPL

The REPL (Read Eval Print Loop) can be accessed anytime the application is paused by typing the repl command.

It will open the Node REPL (the same used in the node console) at the current scope and line, and every local variable can be inspected or even updated in real-time.

debugger example

The bigger advantage of using the debugger to inspect variables is that it allows not only to read a variable but also to check every other variable or function call on the current scope, providing a much deeper understanding of the application behavior.

When simply printing is not enough, we can also use any methods from console to help inspect objects, like console.table for example.

break in routes/index.js:11
  9   const recipes = await readJson(filename);
 10
>11   res.render('index', { title: 'Sample express app', recipes });
 12 });
 13
debug> repl
Press Ctrl+C to leave debug repl
> console.table(recipes, ['title', 'image'])
< ┌─────────┬──────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
< │ (index) │    title     │                                                                           image                                                                           │
< ├─────────┼──────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
< │    0    │   'Black'    │                   'https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG'                   │
< │    1    │   'Latte'    │ 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Latte_at_Doppio_Ristretto_Chiang_Mai_01.jpg/509px-Latte_at_Doppio_Ristretto_Chiang_Mai_01.jpg' │
< │    2    │ 'Cappuccino' │                               'https://upload.wikimedia.org/wikipedia/commons/e/ed/Wet_Cappuccino_with_heart_latte_art.jpg'                               │
< └─────────┴──────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
<

To exit the REPL and return to debug console, type Ctrl + C.

Setting breakpoints

We already mentioned that a debugger statement found in the code is a breakpoint. But we can also set breakpoints through the debugger console using the setBreakpoint|sb command.

  • Set a line breakpoint (setBreakpoint(n), sb(n))

    Adds a breakpoint at the provided line number n in the current script.

  • Set a function breakpoint (setBreakpoint('fname()'), sb('fname()'))

    Adds a breakpoint at the first line of the function identified by fname().

  • Set a script breakpoint (setBreakpoint('filename.js', n), sb('filename.js', n)

    Adds a breakpoint at the nth line of the script identified by filename.js.

Manually adding breakpoints from the debug console can be very useful when debugging callbacks, which cannot be easy stepped into.

break in routes/index.js:9
  7   debugger
  8   const filename = 'coffee-recipes.json';
> 9   readJson(filename)
 10     .then((recipes) => {
 11       res.render('index', { title: 'Sample express app', recipes });
debug> setBreakpoint(11)
  6 router.get('/', (_req, res) => {
  7   debugger
  8   const filename = 'coffee-recipes.json';
  9   readJson(filename)
 10     .then((recipes) => {
>11       res.render('index', { title: 'Sample express app', recipes });
 12     });
 13 });
 14

Watching expressions

“Watching expressions” are used to monitor the changes on a variable without the need to enter the REPL and type again and again.

To start watching, write watch('expression') where expression is any javascript expressions, like variable names, function calls, etc.

break in routes/index.js:7
  5 /* GET home page. */
  6 router.get('/', async (_req, res) => {
> 7   debugger
  8   const filename = 'coffee-recipes.json';
  9   const recipes = await readJson(filename);
debug> watch('filename')
debug> watch('recipes?.[0]?.title')

On the next steps, the watched expressions result will be visible, showing every change.

break in routes/index.js:9
Watchers:
  0: filename = 'coffee-recipes.json'
  1: recipes?.[0]?.title = undefined

  7   debugger
  8   const filename = 'coffee-recipes.json';
> 9   const recipes = await readJson(filename);
 10
 11   res.render('index', { title: 'Sample express app', recipes });
debug> n
break in routes/index.js:11
Watchers:
  0: filename = 'coffee-recipes.json'
  1: recipes?.[0]?.title = 'Black'

  9   const recipes = await readJson(filename);
 10
>11   res.render('index', { title: 'Sample express app', recipes });
 12 });
 13

Using the Chrome Web Tools

Some may say that the node debugger doesn’t have many features in comparison with other languages, or that is hard to use. But that is probably because there is a full-featured alternative that (almost) everyone has access to, as we can use the Chrome Web Tools to debug and inspect Node.js applications, much like we do in the frontend.

For that, we will start the V8 inspector, which will wait for incoming client applications to connect, and use a standard Chromium browser, like Google Chrome, Chromium, Brave, etc.

To start, run your application with the --inspect flag.

node --inspect bin/index.js

Like when we run node inspect, this command will start the inspector and listen on the default port 9229, but instead of also opening the debugger console it will only wait for client connections.

Then, open your Chromium browser and type chrome:inspect in the address bar.

Make sure the Discover network targets option is enabled and then you shall see your application listed under Remote Target. Click on the inspect link to start debugging.

chrome web tools example

This gives you the full potential of the debug features of Chrome Web Tools, including step-by-step debugging, breakpoints, and watch expressions and yet in-source inspecting, folding objects, console, syntax highlight, source maps, etc.

For more on how to debug using the Dev Tools check our Debugging in Frontend post.

Conclusion

So, as we can see, Node.js offers plenty of options to debug your backend applications.

For people more used to a terminal, the command line debugger will suffice in most cases, while the Chrome Web Tools are a great option too, especially for those who prefer a more visual solution. You can also use your editor of choice or IDE and the included debug tools or plugins with a few configuration steps.

I personally prefer the command line debugger, but I still use the Chrome Web Tools when I need a wider look at the scope or when source maps are being used.

Sources and Credits

We are hiring new talent. Check out our careers portal!