Debugging and Profiling
By Syncfusion®, Inc. · Last updated
The debugger statement, source maps, NODE_DEBUG and util.debuglog, the inspector with --inspect and --inspect-brk, debugging in Chrome DevTools and VS Code, breakpoints and watch expressions, remote debugging safety, measuring time with perf_hooks, performance.now, process.hrtime.bigint, marks, measures and PerformanceObserver, event loop delay and utilization, CPU profiling with process.cpuUsage, --cpu-prof, --prof and flame graphs, memory with process.memoryUsage, heap and RSS, garbage collection, --max-old-space-size, heap snapshots and common leaks, and diagnostic reports.
Debugging and Profiling
Debugging = finding out why code does something wrong.
Profiling = finding out where code spends its time and memory.
console.loghelps, but it is slow to add and remove.- Node.js has a full inspector: pause the code and look at every variable.
- It also has tools to measure time, CPU and memory.
Simple lesson: debug to find wrong results, profile to find slow or hungry code.
A Real-World Example
Think of a doctor's check-up.
- First, the doctor asks where it hurts. That is reading the error.
- Then she listens with a stethoscope while you breathe. That is pausing at a breakpoint.
- If you get tired easily, she checks your pulse and weight over time. That is profiling.
What You Will Learn
In this lesson, you will learn:
- The
debuggerstatement and source maps - Extra logs with
NODE_DEBUGandutil.debuglog - The inspector:
--inspect, Chrome DevTools and VS Code - Breakpoints, watch expressions and safe remote debugging
- Measuring time with
perf_hooks - Event loop delay and utilization
- CPU profiles and flame graphs
- Memory: heap, RSS, garbage collection and leaks
- Heap snapshots and diagnostic reports
Debugging Basics
debugger;= a line that pauses the program, but only when a debugger is attached. Otherwise Node.js skips it.- Source map = a file that links built code back to your original code, such as TypeScript lines.
--enable-source-maps= makes stack traces show your original file names and lines.
NODE_DEBUG and util.debuglog
- Many core modules can print extra logs:
NODE_DEBUG=http,net node app.js. util.debuglog("section")= your own logs, shown only whenNODE_DEBUGincludes that section.- Keep them in the code. They cost nothing when turned off.
Send debug output to stderr with console.error or debuglog, so it never mixes with the program's real output.
- Remove
NODE_DEBUG=feesfrom the command in your head, and theFEESline would disappear. - On Windows PowerShell, set it first:
$env:NODE_DEBUG="fees"; node app.js.
Simple lesson: leave debuglog lines in your code and switch them on with NODE_DEBUG.
The Inspector
Inspector = a built-in debugging server that tools like Chrome DevTools and VS Code connect to.
node --inspect app.js= start normally, and let a debugger connect.node --inspect-brk app.js= pause on the first line, so you can set breakpoints before anything runs.- Node.js prints a
ws://127.0.0.1:9229/...address. The debugger connects there.
Animation: node --inspect opens port 9229, Chrome DevTools or VS Code connects, and the program pauses at a breakpoint node --inspect127.0.0.1:9229 Chrome DevTools VS Code
paused at line 12
The debugger connects to the inspector and pauses your program where you ask.
Debugging with Chrome DevTools
- Run
node --inspect-brk app.js. - Open Chrome and go to
chrome://inspect. - Click inspect under your script.
- Click a line number to set a breakpoint, then press Resume.
- When the code pauses, read the variables in the Scope panel.
Debugging in VS Code
- Open the project folder in VS Code.
- Click a line number to set a breakpoint (a red dot).
- Open the Run and Debug panel and choose Node.js.
- Or open a JavaScript Debug Terminal and run
node app.jsthere.
Breakpoints and Watch Expressions
- Breakpoint = a line where the program pauses.
- Conditional breakpoint = pauses only when a condition is true, such as
marks < 35. - Watch expression = a value the debugger shows at every pause, such as
total / count. - Step over / into / out = run the next line, go inside a function, or finish it.
Remote Debugging Safety
- Anyone who can reach the inspector port can run any code on your computer.
- Keep the default address
127.0.0.1(this computer only). - Never use
--inspect=0.0.0.0on a server reachable from the internet. - To debug a remote server, use an SSH tunnel to its port 9229.
Never leave --inspect on in production. Anyone who reaches the port controls the process.
Simple lesson: --inspect-brk plus a breakpoint shows you every variable at the moment things go wrong.
Performance Measurement
node:perf_hooks and the global performance object measure time.
performance.now()= milliseconds since the program started, with decimals. Good for timing.process.hrtime.bigint()= nanoseconds as abigint. Very precise.performance.mark(name)= a named point in time.performance.measure(name, start, end)= the time between two marks.PerformanceObserver= get told about new marks and measures.
- Your numbers change every run. Measure several times before you decide which is faster.
Profile before you optimise. The slow part is often not where you guess.
Event Loop Delay and Utilization
- Event loop delay = how late timers run. High delay means something blocks the loop.
monitorEventLoopDelay()= records the delay as a histogram, withmean,maxand percentiles.performance.eventLoopUtilization()= how busy the loop was, from 0 (idle) to 1 (always busy).
// main.mjs
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
const before = performance.eventLoopUtilization();
// Block the loop for 300 ms in small bursts
const end = Date.now() + 300;
const busy = () => {
const stop = Date.now() + 50;
while (Date.now() < stop) {}
if (Date.now() < end) setTimeout(busy, 0);
else report();
};
busy();
function report() {
h.disable();
const elu = performance.eventLoopUtilization(before);
console.log("Max loop delay over 40 ms?", h.max / 1e6 > 40);
console.log("Loop busy most of the time?", elu.utilization > 0.5);
}
Output:
Max loop delay over 40 ms? true
Loop busy most of the time? true
Simple lesson: time code with performance.now, and watch event loop delay in servers.
Measuring time: take performance.now() before and after the work and subtract
start = performance.now() end = performance.now()
the work you measure time taken = end - start (in milliseconds)
Measure with two timestamps, not with guesses.
CPU Profiling
process.cpuUsage()= CPU time used by the process, in microseconds, split intouserandsystem.node --cpu-prof app.js= write a.cpuprofilefile when the program ends. Open it in Chrome DevTools, Performance tab.node --prof app.js= the older V8 log. Turn it into text withnode --prof-process.- Flame graph = a picture of the profile. Each bar is a function. Wider bars use more time. Bars on top are called by bars below.
report.js
let total = 0;
for (let i = 0; i < 2_000_000; i++) total += Math.sqrt(i);
console.log("Report total:", Math.round(total));
$ node --cpu-prof --cpu-prof-dir=profiles report.js && ls profiles
Report total: 1885617376
CPU-xxxx.cpuprofile
Simple lesson: a CPU profile shows which functions eat the time. Fix the widest bars first.
Memory Analysis
process.memoryUsage()= memory numbers in bytes:rss= all memory the process holds from the operating system.heapTotalandheapUsed= memory for JavaScript objects.external= memory for things like Buffers.
- Garbage collection (GC) = V8 frees objects that nothing points to any more.
--max-old-space-size=2048= allow the heap to grow to 2048 MB. Without enough room, Node.js crashes with "heap out of memory".
Common Leak Patterns
Memory leak = memory that keeps growing because the program keeps references it no longer needs.
- A cache or
Mapthat only ever grows. - Event listeners added again and again and never removed.
- Timers with
setIntervalthat are never cleared. - Big objects kept in a variable that lives forever, such as a module-level array.
Heap Snapshots and Finding Leaks
- Heap snapshot = a photo of every object in memory and what holds on to it.
- Make one with
v8.writeHeapSnapshot(), or from the Memory tab of Chrome DevTools. - Snapshot comparison = take one snapshot, do the suspect action many times, take another. Compare them. Objects whose count keeps rising are the leak.
snapshot.js
const v8 = require("node:v8");
const keep = [];
for (let i = 0; i < 10000; i++) keep.push({ roll: i });
const file = v8.writeHeapSnapshot();
console.log("Snapshot written:", file.endsWith(".heapsnapshot"));
$ node snapshot.js && ls *.heapsnapshot
Snapshot written: true
Heap-xxxx.heapsnapshot
grow.js
const keep = [];
while (true) keep.push(new Array(10000).fill("leak"));
$ node --max-old-space-size=16 grow.js
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
Simple lesson: if memory keeps rising, compare heap snapshots and look for what never gets freed.
Diagnostic Reports (Advanced)
Diagnostic report = a JSON file with the state of the process: stack, memory, versions and more.
process.report.writeReport()= write one now.node --report-on-fatalerror app.js= write one if Node.js crashes from a fatal error, such as heap out of memory.--report-uncaught-exceptionand--report-on-signalalso exist.- Attach the report when you ask for help with a crash.
report.js
const file = process.report.writeReport("report.json");
const report = require("node:fs").readFileSync(file, "utf8");
console.log("Report has Node.js version?", JSON.parse(report).header.nodejsVersion.startsWith("v"));
require("node:fs").rmSync(file);
$ node report.js
Report has Node.js version? true
Writing Node.js report to file: report.json
Node.js report completed
Simple lesson: a diagnostic report is a snapshot of the whole process, for crashes you cannot repeat.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Measuring once | One run tells the truth | Measure several runs | Random noise decides |
Leaving debugger; in code | It always pauses | It only pauses with a debugger attached, but it is clutter | Confusing code, surprise pauses |
--inspect=0.0.0.0 on a server | The port is private | Keep 127.0.0.1 and use a tunnel | Strangers can run code on your server |
| A cache with no size limit | Memory is unlimited | Limit it or expire entries | Memory grows until the crash |
Raising --max-old-space-size to hide a leak | More memory fixes it | Find the leak with snapshots | The crash just comes later |
| Profiling in watch or debug mode | Timings are the same | Profile a normal run | Wrong conclusions |
TL;DR
debugger;+--inspect-brk= pause and look at variables.- Chrome DevTools (
chrome://inspect) or VS Code connect to the inspector. NODE_DEBUG=x+util.debuglog("x")= logs you can switch on.performance.now(),process.hrtime.bigint(), marks and measures = timing.- Event loop delay = high values mean something blocks the loop.
--cpu-prof= CPU profile. Wide bars in the flame graph are slow.process.memoryUsage()= rss, heapTotal, heapUsed, external.- Leaks = growing caches, listeners, intervals. Compare heap snapshots.
- Diagnostic report = JSON snapshot of the process for crashes.
Check Yourself
Exercises
What to Remember
- Use
--inspect-brkand breakpoints to see variables at the moment of a bug. - Keep the inspector on
127.0.0.1. debugloglines stay in the code and switch on withNODE_DEBUG.- Time code with
performance.now(), and run it several times. - CPU profiles and flame graphs show where time goes.
- Leaks are things that grow forever. Compare heap snapshots to find them.
Next: Cryptography and Security. You will hash passwords, encrypt data and protect your app from common attacks.