# Debugging and Profiling

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: Quality and Production
Source: https://www.devtutorials.ai/learn/nodejs/quality-production/debugging-and-profiling

> 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.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 200" role="img" ariaLabelledby="fig-node-inspector" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-inspector">Animation: node --inspect opens port 9229, Chrome DevTools or VS Code connects, and the program pauses at a breakpoint</title>
<rect x="20" y="70" width="160" height="60" rx="10"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/><text x="100" y="96" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>node --inspect</text><text x="100" y="116" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 11 }}>127.0.0.1:9229</text>
<rect x="380" y="20" width="160" height="50" rx="10"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="460" y="50" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Chrome DevTools</text>
<rect x="380" y="130" width="160" height="50" rx="10"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="460" y="160" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>VS Code</text>
<line x1="180" y1="95" x2="380" y2="45"  style={{ stroke: "var(--ngc-slate)", strokeDasharray: "4 4" }}/><line x1="180" y1="105" x2="380" y2="155"  style={{ stroke: "var(--ngc-slate)", strokeDasharray: "4 4" }}/>
<g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.5;0.9;1" dur="5s" repeatCount="indefinite" />
<rect x="215" y="85" width="130" height="30" rx="6"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }}/><text x="280" y="105" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>paused at line 12</text>
</g>
<circle r="6" cx="380" cy="45" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="380;180;180" keyTimes="0;0.4;1" dur="5s" repeatCount="indefinite" /><animate attributeName="cy" values="45;95;95" keyTimes="0;0.4;1" dur="5s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>The debugger connects to the inspector and pauses your program where you ask.</figcaption>
</figure>

### 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).

```js
// 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:

```text
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.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 170" role="img" ariaLabelledby="fig-node-perf" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-perf">Measuring time: take performance.now() before and after the work and subtract</title>
<line x1="30" y1="90" x2="530" y2="90"  style={{ stroke: "var(--ink)", strokeWidth: 2 }}/>
<line x1="120" y1="70" x2="120" y2="110"  style={{ stroke: "var(--ngc-blue)", strokeWidth: 2 }}/><text x="120" y="60" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>start = performance.now()</text>
<line x1="440" y1="70" x2="440" y2="110"  style={{ stroke: "var(--ngc-blue)", strokeWidth: 2 }}/><text x="440" y="130" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>end = performance.now()</text>
<rect x="120" y="86" width="0" height="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }}><animate attributeName="width" values="0;320;320" keyTimes="0;0.6;1" dur="5s" repeatCount="indefinite" /></rect>
<text x="280" y="80" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>the work you measure</text>
<text x="280" y="160" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>time taken = end - start   (in milliseconds)</text>
</svg>
<figcaption>Measure with two timestamps, not with guesses.</figcaption>
</figure>

## 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`

```js
let total = 0;
for (let i = 0; i < 2_000_000; i++) total += Math.sqrt(i);
console.log("Report total:", Math.round(total));
```

```text
$ 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`

```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"));
```

```text
$ node snapshot.js && ls *.heapsnapshot
Snapshot written: true
Heap-xxxx.heapsnapshot
```

`grow.js`

```js
const keep = [];
while (true) keep.push(new Array(10000).fill("leak"));
```

```text
$ 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`

```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);
```

```text
$ 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

<div className="tscroll">

| 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 |

</div>

## 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.
