Debugging Node.js: the complete practical guide

By

Debug Node.js with Chrome DevTools, VS Code, breakpoints, source maps, CPU and heap profiles, inspector ports, child processes, and safe remote access.

~~~

console.log() is useful, but sometimes you need to stop the program and look around.

Node includes the V8 Inspector. It lets Chrome DevTools, VS Code, and other debugging clients attach to a running Node process.

You can pause code, inspect variables, step through functions, follow async calls, and record CPU or memory profiles.

Start the Node inspector

Run your program with --inspect:

node --inspect server.js

Node starts the application and opens the inspector on port 9229.

The terminal prints a WebSocket URL similar to this:

Debugger listening on ws://127.0.0.1:9229/...

The long identifier changes every time. Debugging tools discover it automatically.

Attach Chrome DevTools

Open this page in Chrome:

chrome://inspect

Older examples sometimes use about://inspect. Chrome redirects it to the same page.

Under Remote Target, find your Node process and click inspect.

Chrome opens a dedicated DevTools window. The Sources panel contains your Node files instead of a web page’s scripts.

Pause before the program starts

--inspect starts the code immediately. A startup bug might happen before you attach.

Use --inspect-brk to pause on the first line:

node --inspect-brk server.js

Attach the debugger, add any breakpoints you need, then resume execution.

Current Node releases also support --inspect-wait:

node --inspect-wait server.js

This waits for a debugger before execution but does not force a first-line breakpoint.

Use:

Add a breakpoint in DevTools

Open a file in the Sources panel and click its line number.

Reload or repeat the action that reaches that line. Node pauses before executing it.

While paused, DevTools shows:

Hover over a variable to see its current value.

Use the debugger statement

You can place a breakpoint directly in the code:

function calculateTotal(items) {
  debugger
  return items.reduce((total, item) => total + item.price, 0)
}

Node only pauses there when a debugger is attached.

Remove debugger statements when the investigation is over. A connected debugger can still stop on one in production code.

Step through code

The debugger offers several ways to continue:

Start with Step over. Step into only when the bug appears to live inside the called function.

This keeps you out of framework and dependency internals unless you need them.

Use conditional breakpoints

A loop might run thousands of times before the wrong value appears.

Right-click a line number and add a conditional breakpoint:

customer.id === 'cus_42'

DevTools pauses only when the expression is true.

You can also add a logpoint. It writes a message without changing the source file or stopping execution.

Inspect asynchronous code

Promises, timers, and event handlers move work across the event loop. That can make the immediate call stack look incomplete.

Chrome DevTools can show async stack traces. They connect the current callback to the code that scheduled it.

Consider this example:

async function loadCustomer(id) {
  const response = await fetch(`https://api.test/customers/${id}`)
  return response.json()
}

Put a breakpoint after await. The async stack helps you find the caller that started loadCustomer().

Enable Pause on caught exceptions only when needed. Many libraries catch expected errors internally, so enabling it can create a lot of noise.

Pause on exceptions

The Sources panel includes a pause-on-exceptions control.

The useful modes are:

Start with uncaught exceptions. This stops near the original failure instead of much later when a rejected promise or error reaches a generic handler.

Debug an npm script

Pass Node flags through the NODE_OPTIONS environment variable:

NODE_OPTIONS='--inspect-brk' npm run dev

This works when the npm script launches Node directly.

Another option is to change the script temporarily:

{
  "scripts": {
    "debug": "node --inspect-brk server.js"
  }
}

Then run:

npm run debug

Use a different inspector port

Each inspected process needs its own port.

node --inspect=9230 worker.js

Use port 0 to let Node choose an available port:

node --inspect=0 worker.js

The chosen address appears in the terminal output.

This is handy for test runners and child processes that may run in parallel.

Debug child processes

A child Node process does not automatically share its parent’s inspector session.

Pass an inspector flag through execArgv when using fork():

import { fork } from 'node:child_process'

const child = fork('./worker.js', [], {
  execArgv: ['--inspect=0'],
})

Each child gets its own inspector endpoint.

When a framework starts workers for you, check its debugging options. Starting every worker on port 9229 creates a collision.

Debug worker threads

Chrome DevTools can display worker threads associated with the inspected process.

In the Sources panel, use the thread selector to switch execution contexts. Breakpoints in a worker must be set in that worker’s loaded script.

Workers can stop independently, so always check which thread is currently paused.

Debug TypeScript with source maps

The debugger runs the JavaScript emitted by TypeScript.

Source maps connect that JavaScript back to the original .ts files.

Enable them in tsconfig.json:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

Then start the compiled entry file with source maps enabled:

node --enable-source-maps --inspect-brk dist/server.js

You should now see the TypeScript source and get TypeScript locations in stack traces.

Bundlers and runtime TypeScript tools have their own source-map settings. Check that they emit accurate maps before blaming the debugger.

Attach VS Code

VS Code speaks the same inspector protocol.

The quickest workflow is to open Run and Debug, choose Node.js, and launch the current file.

For a repeatable configuration, create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug server",
      "program": "${workspaceFolder}/server.js",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

Use request: "attach" when Node is already running with --inspect.

Inspect values without stopping

Sometimes you only need a clearer object representation.

Node’s console.dir() lets you control depth and colors:

console.dir(customer, {
  depth: null,
  colors: true,
})

Read how to inspect JavaScript objects for more options.

Use breakpoints when the important question is how a value changed, not only what it contains now.

Record a CPU profile

When a process uses too much CPU, open the Performance panel in DevTools.

Start recording, reproduce the slow operation, then stop.

The profile shows which functions consumed execution time. Focus on wide or repeated frames in the flame chart.

Keep the recording short. A focused reproduction is easier to understand than several minutes of unrelated activity.

Node can also write a CPU profile without attaching DevTools:

node --cpu-prof server.js

Open the generated profile in Chrome DevTools later.

Investigate memory usage

Use the Memory panel to take heap snapshots.

A practical leak investigation looks like this:

  1. Start the application in a stable state.
  2. Take a baseline snapshot.
  3. Repeat the suspected leaking action several times.
  4. Force garbage collection when your debugging setup allows it.
  5. Take another snapshot and compare retained objects.

Look at retaining paths. They tell you why an object is still reachable.

Do not assume every increase is a leak. Caches, connection pools, and lazy initialization can grow once and then stabilize.

Debug a process that is already running

On POSIX systems, Node can start the inspector after receiving SIGUSR1:

kill -USR1 12345

Replace 12345 with the Node process ID.

This is useful when you did not start the process with --inspect. Be careful: opening a debugger on a live service changes its security and operational risk.

Never expose the inspector publicly

The inspector provides powerful access to the running process. A connected client can execute code and read application data.

Keep it bound to the loopback interface:

node --inspect=127.0.0.1:9229 server.js

Do not bind it to 0.0.0.0 on an internet-accessible machine.

For remote debugging, use an authenticated tunnel such as SSH and keep the inspector itself local.

Common debugging problems

The process does not appear in Chrome

Check that Node printed a debugger URL. Confirm the port and add it under Configure on chrome://inspect if discovery does not find it.

The breakpoint is hollow or ignored

The loaded code may not match the file you opened. Check source maps, build output, and whether another process is serving an older file.

The app runs before I can attach

Use --inspect-brk or --inspect-wait.

The inspector port is already in use

Choose another port or use --inspect=0.

Stepping enters Node internals

Enable blackboxing in DevTools or use skipFiles in VS Code.

My debugging workflow

I start with the smallest reproducible action.

Then I add one breakpoint before the value becomes wrong and one after it. I inspect the arguments, step over each operation, and move the first breakpoint closer once I know which branch causes the problem.

For startup failures I use --inspect-brk. For slow code I record a short CPU profile. For suspected leaks I compare heap snapshots after repeating one action.

The official Node debugger documentation lists the current inspector flags and command-line debugger.

Tagged: Node.js · All topics
~~~

Related posts about node: