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.log helps, 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:

  1. The debugger statement and source maps
  2. Extra logs with NODE_DEBUG and util.debuglog
  3. The inspector: --inspect, Chrome DevTools and VS Code
  4. Breakpoints, watch expressions and safe remote debugging
  5. Measuring time with perf_hooks
  6. Event loop delay and utilization
  7. CPU profiles and flame graphs
  8. Memory: heap, RSS, garbage collection and leaks
  9. 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 when NODE_DEBUG includes 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=fees from the command in your head, and the FEES line 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

  1. Run node --inspect-brk app.js.
  2. Open Chrome and go to chrome://inspect.
  3. Click inspect under your script.
  4. Click a line number to set a breakpoint, then press Resume.
  5. When the code pauses, read the variables in the Scope panel.

Debugging in VS Code

  1. Open the project folder in VS Code.
  2. Click a line number to set a breakpoint (a red dot).
  3. Open the Run and Debug panel and choose Node.js.
  4. Or open a JavaScript Debug Terminal and run node app.js there.

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.0 on 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 a bigint. 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, with mean, max and 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 into user and system.
  • node --cpu-prof app.js = write a .cpuprofile file when the program ends. Open it in Chrome DevTools, Performance tab.
  • node --prof app.js = the older V8 log. Turn it into text with node --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.
    • heapTotal and heapUsed = 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 Map that only ever grows.
  • Event listeners added again and again and never removed.
  • Timers with setInterval that 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-exception and --report-on-signal also 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

MistakeWrong ideaCorrect ideaWhat goes wrong
Measuring onceOne run tells the truthMeasure several runsRandom noise decides
Leaving debugger; in codeIt always pausesIt only pauses with a debugger attached, but it is clutterConfusing code, surprise pauses
--inspect=0.0.0.0 on a serverThe port is privateKeep 127.0.0.1 and use a tunnelStrangers can run code on your server
A cache with no size limitMemory is unlimitedLimit it or expire entriesMemory grows until the crash
Raising --max-old-space-size to hide a leakMore memory fixes itFind the leak with snapshotsThe crash just comes later
Profiling in watch or debug modeTimings are the sameProfile a normal runWrong conclusions

TL;DR

  1. debugger; + --inspect-brk = pause and look at variables.
  2. Chrome DevTools (chrome://inspect) or VS Code connect to the inspector.
  3. NODE_DEBUG=x + util.debuglog("x") = logs you can switch on.
  4. performance.now(), process.hrtime.bigint(), marks and measures = timing.
  5. Event loop delay = high values mean something blocks the loop.
  6. --cpu-prof = CPU profile. Wide bars in the flame graph are slow.
  7. process.memoryUsage() = rss, heapTotal, heapUsed, external.
  8. Leaks = growing caches, listeners, intervals. Compare heap snapshots.
  9. Diagnostic report = JSON snapshot of the process for crashes.

Check Yourself

Exercises

What to Remember

  • Use --inspect-brk and breakpoints to see variables at the moment of a bug.
  • Keep the inspector on 127.0.0.1.
  • debuglog lines stay in the code and switch on with NODE_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.