Debugging Node.js Applications — Inspector, Chrome DevTools, and VS Code
Node.js debugging mastery: Chrome DevTools inspector, VS Code debugger, CPU profiling, heap snapshots, async stack traces, and diagnosing memory leaks in production..
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
Node.js includes a built-in inspector protocol accessible via --inspect and --inspect-brk flags. The Chrome DevTools debugger attaches to this protocol and provides step-through debugging, console pro
Debugging Node.js is like being a detective in a busy kitchen. You have a recipe (your code), but the dish isn't coming out right. The Inspector is your magnifying glass—it lets you pause the action, peek at ingredients (variables), and step through each cooking step (line of code). Chrome DevTools is your surveillance camera showing real-time kitchen activity. VS Code is your detective's notebook where you can set breakpoints like 'stop here and check the pot.' Without these tools, you're just guessing which ingredient is spoiled.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
console.log debugging works until your server handles 500 requests per second and you realise logging every variable doubles response time. Node.js ships with a production-grade debugger that most developers never open. The inspector protocol gives you heap snapshots, CPU flame graphs, and step-through debugging without any third-party tools. This article covers the full debugging workflow — from attaching the inspector to diagnosing memory leaks with heap comparisons — using only tools shipped with Node.js and VS Code.
Why Node.js Debugging Is Different
Debugging Node.js is not like debugging client-side JavaScript. You don't have a browser DOM, and your code runs on a server with limited visibility. Traditional console.log debugging works for trivial cases but fails in production where you need to inspect async flows, memory leaks, or race conditions. Node.js provides a built-in inspector protocol that allows external tools to attach and control execution. Understanding this protocol is the foundation for effective debugging. The inspector exposes endpoints for breakpoints, stepping, profiling, and heap snapshots. Tools like Chrome DevTools and VS Code use this protocol under the hood. Without this knowledge, you're flying blind when things break in production.
Setting Up Chrome DevTools for Node.js
Chrome DevTools is the most feature-rich debugger for Node.js. To attach, start your app with --inspect and open chrome://inspect in Chrome. You'll see a list of remote targets. Click 'inspect' to open dedicated DevTools. The Sources tab shows your file system, allows breakpoints, and supports step-through debugging. The Console tab runs in the Node.js context. The Memory and Profiler tabs are invaluable for performance debugging. One common pitfall: if you use --inspect-brk, the app won't start until you attach the debugger. This is useful for debugging startup issues. For production debugging, use --inspect without --inspect-brk to avoid blocking.
VS Code Integrated Debugger
VS Code's debugger is the most convenient for daily development. It integrates directly with your editor, allowing you to set breakpoints, inspect variables, and watch expressions without leaving your code. To configure, create a .vscode/launch.json file with a Node.js launch configuration. The key configuration is 'type': 'node' and 'request': 'launch' or 'attach'. Use 'attach' to connect to an already running process (e.g., with --inspect). The 'preLaunchTask' option can run a build step before debugging. VS Code also supports conditional breakpoints, logpoints (console.log without code changes), and hit counts. For monorepos, use 'runtimeArgs' to specify the entry point.
Advanced Breakpoints and Stepping
Breakpoints are the core of debugging. Beyond simple line breakpoints, Node.js supports conditional breakpoints (break only when a condition is true), logpoints (log a message without pausing), and hit count breakpoints (break after N hits). In Chrome DevTools, right-click the line number to set a conditional breakpoint. In VS Code, use the breakpoint context menu. Stepping options: Step Over (F10) executes the current line and moves to the next; Step Into (F11) goes into a function call; Step Out (Shift+F11) finishes the current function and returns. Use 'Continue' (F8) to resume execution until the next breakpoint. For async code, be aware that stepping may jump between async contexts.
Debugging Asynchronous Code and Promises
Async code is where Node.js debugging gets tricky. Callbacks, Promises, and async/await create execution contexts that can be hard to trace. The inspector handles async stacks well: when you break inside an async function, the call stack shows the async context chain. In Chrome DevTools, enable 'Async' in the Call Stack panel to see the full async trace. For Promises, use the 'Promise' tab to inspect pending and settled promises. A common bug: unhandled Promise rejections. Node.js will warn about these, but you can break on them by enabling 'Pause on uncaught exceptions' in the debugger. For async/await, stepping works as expected, but be careful with await inside loops — it can cause performance issues.
Profiling CPU and Memory
Performance issues often require profiling. Node.js's inspector provides CPU and heap profilers. In Chrome DevTools, the Performance tab records CPU samples and shows a flame graph. The Memory tab can take heap snapshots and compare them to find leaks. To start profiling programmatically, use the built-in inspector module. For CPU profiling, use console.profile() and console.profileEnd(). For heap snapshots, use the v8 module's getHeapSnapshot(). In production, use the --prof flag to generate a V8 log file, then process it with --prof-process. Common issues: memory leaks from closures, large buffers not garbage collected, and CPU spikes from synchronous operations.
Debugging in Production with --inspect
Debugging in production is risky but sometimes necessary. The safest approach is to use --inspect with a secure WebSocket (wss://) and authentication. Alternatively, use the inspector protocol over a Unix socket to avoid network exposure. Tools like ndb (by Google) provide a secure debugging UI. For containerized apps, expose the inspector port only to a debug sidecar. Never use --inspect without a firewall. A common pattern: start the app with --inspect=0.0.0.0:9229 but bind to localhost in production. Use SSH tunneling to connect: ssh -L 9229:localhost:9229 user@prod-server. Then attach Chrome DevTools to localhost:9229.
Sometimes you can't attach a debugger — especially in serverless or ephemeral environments. In those cases, structured logging and distributed tracing are your friends. Use libraries like pino or winston for JSON logging with log levels. Include request IDs, correlation IDs, and timing information. For tracing, use OpenTelemetry to instrument your code and send traces to a backend like Jaeger or Zipkin. This allows you to see the full lifecycle of a request across services. When debugging, add logpoints (via VS Code or Chrome) that log without code changes. In production, use dynamic log levels: change log level at runtime without restarting the app.
Common Pitfalls and How to Avoid Them
Even with great tools, developers make mistakes. One common pitfall: debugging minified or transpiled code. Always use source maps. In production, ensure your build process generates source maps and uploads them to your error tracking service. Another pitfall: debugging in a cluster or with child processes. The inspector attaches to the main process only. To debug child processes, start them with --inspect and a different port. Use the 'inspect' option in child_process.fork(). Also, beware of breakpoints in hot code paths — they can cause timeouts in production. Use conditional breakpoints sparingly. Finally, don't rely solely on the debugger; combine it with logging and monitoring.
Putting It All Together: A Debugging Workflow
Effective debugging is a workflow. Start with logging: add structured logs at key points. If the issue is reproducible, attach the debugger locally. Use conditional breakpoints to narrow down the problem. For performance issues, profile CPU and memory. For production issues, use SSH tunneling to attach the debugger safely, or rely on logs and tracing. Always have a hypothesis before debugging. Use the scientific method: form a hypothesis, test it with a breakpoint or log, and iterate. Document your findings. After fixing, add a test to prevent regression. Finally, monitor the fix in production with alerts.
CLI Debugger: node inspect Commands, Watchers, and REPL
The built-in CLI debugger is often overlooked but invaluable for quick debugging without an IDE. Start with node inspect app.js. This drops you into a debugger prompt where you can use commands like cont (continue), next, step, out, pause, backtrace, and list(n). Use watch('expression') to monitor variables; watchers lists them. The debugger also provides a REPL: type repl to evaluate expressions in the current scope. For example, watch('req.url') and then repl to inspect req.headers. Exit REPL with Ctrl+D. This is perfect for serverless or CI environments where Chrome DevTools isn't available. The CLI debugger respects the same --inspect protocol, so you can attach later. Pro tip: use node inspect --inspect-brk app.js to break on first line.
--inspect only if necessary and secured.watch() and repl to inspect state.launch.json Advanced Configuration for Node.js
VS Code's launch.json is more powerful than most realize. Beyond basic type: node, you can configure runtimeArgs for --inspect-brk, env for environment variables, cwd for working directory, outputCapture to capture stdout/stderr, and skipFiles to skip node_modules or internal Node.js files. Use preLaunchTask to run a build step before debugging. For TypeScript, set preLaunchTask: "tsc: build - tsconfig.json" and outFiles to map compiled JS. The resolveSourceMapLocations option controls which source maps are resolved. Example: "resolveSourceMapLocations": ["${workspaceFolder}/", "!/node_modules/**"]. You can also use localRoot and remoteRoot for remote debugging. Pro tip: use console: "integratedTerminal" to see console output in the terminal instead of the debug console.
"skipFiles": ["<node_internals>/**"] to avoid stepping into Node.js internal code. It saves time.preLaunchTask that modifies production code. Keep build steps separate.launch.json with skipFiles, preLaunchTask, and resolveSourceMapLocations for efficient debugging.Multi-Target Debugging: Debug Multiple Processes Simultaneously
Modern Node.js apps often run multiple processes (e.g., microservices, worker threads, or a frontend + backend). VS Code supports multi-target debugging via a compound configuration. Define each process as a separate launch configuration, then create a compound that runs them together. Example: debug an Express API and a WebSocket server concurrently. Use "stopAll": true to stop all when one stops. For worker threads, use "autoAttachChildProcesses": true in the parent config. This attaches the debugger to any child processes spawned. Alternatively, use "attach" configurations for processes already running with --inspect. Pro tip: set "restart": true to automatically reattach if the process restarts (useful with nodemon).
--inspect process needs a unique port. Default is 9229, so specify --inspect=9230 for the second.Source Maps for TypeScript: Debug .ts Files Directly
Debugging TypeScript requires source maps to map compiled JavaScript back to TypeScript. Enable "sourceMap": true in tsconfig.json. In VS Code, set outFiles to the output directory and ensure resolveSourceMapLocations includes your source. The debugger will then let you set breakpoints in .ts files. For complex setups (e.g., path mapping, project references), use sourceMapPathOverrides to remap paths. Example: "sourceMapPathOverrides": { "webpack:///./src/": "${workspaceFolder}/src/" } for webpack. Pro tip: always test source maps by setting a breakpoint in a .ts file and verifying it hits. If not, check that the .js file has a //# sourceMappingURL comment.
.ts files are not binding, ensure outFiles matches the actual output path and that source maps are not disabled by a bundler.sourceMap: true in tsconfig and configure outFiles in launch.json to debug TypeScript directly.Remote Debugging Over SSH: Debug Node.js on a Remote Server
When your app runs on a remote server (e.g., staging), you can debug it locally using SSH port forwarding. Start the Node.js process with --inspect=0.0.0.0:9229 (bind to all interfaces) but secure it with a firewall. On your local machine, run ssh -L 9229:localhost:9229 user@remote-server. Then in VS Code, create an attach configuration with "port": 9229. Alternatively, use --inspect-publish-uid=http to restrict to specific IPs. For production, never expose the inspector port publicly; use SSH tunneling or a VPN. Pro tip: use --inspect-brk to pause on first line, then attach. This is great for debugging startup issues.
Heap Snapshot Generation and Memory Leak Debugging
Memory leaks in Node.js can be elusive. Generate heap snapshots using the v8 module: require('v8').getHeapSnapshot().pipe(fs.createWriteStream('heap.heapsnapshot')). Load the snapshot in Chrome DevTools (Memory tab) to compare objects. For automated leak detection, use heapdump package: heapdump.writeSnapshot('/tmp/heap-1.heapsnapshot'). Compare two snapshots to see retained objects. Clinic.js (specifically clinic heapprofiler) provides a flamegraph-like view. Pro tip: trigger snapshots on demand via a signal: process.on('SIGUSR2', () => heapdump.writeSnapshot()). For long-running processes, use --max-old-space-size to set memory limits and --trace-gc to log GC events.
v8.getHeapSnapshot() or heapdump to capture heap snapshots and analyze memory leaks.The Silent Memory Leak: How a Forgotten setInterval Brought Down Our API Gateway
setInterval to cache external service status, but never called clearInterval when the server shut down or the route was called again. Each request created a new interval, each holding a reference to the request object and a large closure.clearInterval on server shutdown (process.on('SIGTERM')). Also added a memory usage alert in our monitoring.- Always clear timers when they are no longer needed, especially in request handlers.
- Use
process.on('SIGTERM')to cleanly shut down resources. - Monitor memory growth trends, not just absolute values.
- Profile heap snapshots regularly in staging to catch leaks early.
| File | Command / Code | Purpose |
|---|---|---|
| start-inspector.sh | node --inspect app.js | Why Node.js Debugging Is Different |
| debug-chrome.sh | node --inspect-brk app.js | Setting Up Chrome DevTools for Node.js |
| .vscode | { | VS Code Integrated Debugger |
| conditional-breakpoint.js | function processOrder(order) { | Advanced Breakpoints and Stepping |
| async-debug.js | async function fetchData(url) { | Debugging Asynchronous Code and Promises |
| profile-cpu.js | const inspector = require('inspector'); | Profiling CPU and Memory |
| ssh-tunnel.sh | node --inspect=127.0.0.1:9229 app.js & | Debugging in Production with --inspect |
| structured-logging.js | const pino = require('pino'); | javascript configuration |
| debug-child.js | const { fork } = require('child_process'); | Common Pitfalls and How to Avoid Them |
| debug-workflow.js | logger.info({ event: 'order_created', orderId }, 'Order created'); | Putting It All Together |
| $ node inspect app.js | CLI Debugger | |
| tsconfig.json | { | Source Maps for TypeScript |
| $ node --inspect=0.0.0.0:9229 app.js | Remote Debugging Over SSH | |
| snapshot.js | const v8 = require('v8'); | Heap Snapshot Generation and Memory Leak Debugging |
Key takeaways
node inspect command provides a full debugger with watchers and REPL, essential for headless environments.skipFiles, preLaunchTask, and resolveSourceMapLocations to streamline TypeScript and complex project debugging.v8.getHeapSnapshot() or heapdump to capture memory snapshots; compare them in Chrome DevTools to find leaks.node inspect with watchers and REPL for debugging in headless environments.--inspect allows secure remote debugging from VS Code.v8.writeHeapSnapshot() and analyze with Chrome DevTools or Clinic.js.Interview Questions on This Topic
How do you start the Node.js Inspector and connect Chrome DevTools?
node --inspect app.js to start the inspector on a default port (9229). Then open chrome://inspect in Chrome, click 'Open dedicated DevTools for Node', and you'll see your app's console, sources, and profiler.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's Node.js. Mark it forged?
6 min read · try the examples if you haven't