# Node.js lokal debuggen und profilieren

URL: https://www.mikebild.dev/de/blog/nodejs-debuggen-und-beobachten/

```js
console.log(x);
console.log('HIER');
console.log('HIER 2');
```

Diese drei Zeilen können genau eine Sache: einen Wert an einer Stelle zeigen, die man vorher erraten hat. Abschalten lassen sie sich nur, indem jemand die Datei wieder anfasst. Den Prozess anhalten können sie nicht. Über Speicherverbrauch sagen sie gar nichts. Für alle drei Lücken bringt Node.js seit Version 6.3 eigene Werkzeuge mit, ohne dass ein zusätzliches Paket installiert werden muss – sie werden nur selten benutzt.

Sortieren lassen sie sich als Eskalationsleiter. Ganz unten steht `console.log` – immer an, unstrukturiert, aber sofort da. Eine Stufe höher liegt das `debug`-Modul, das sich über eine Umgebungsvariable an- und ausschalten lässt, ohne dass ich Code anfasse. Darüber sitzt der eingebaute Inspector mit interaktiven Breakpoints in Chrome DevTools oder VS Code. Und ganz oben, wenn es um Performance oder Speicher geht, nutze ich denselben Inspector als Profiler. Wer diese vier Stufen sauber trennen kann, hört auf, blind zu raten.

## Warum console.log an seine Grenzen kommt

`console.log` hat zwei Eigenschaften, die es im Kleinen praktisch und im Großen unangenehm machen. Erstens ist es immer aktiv: Was einmal im Code steht, wird bei jedem Durchlauf ausgeführt, auch in Produktion, wo es die Ausgabe verschmutzt und je nach Menge sogar Performance kostet. Zweitens ist es unstrukturiert – ich sehe einen Wert, aber ich kann nicht mit einem Schalter entscheiden, ob ich gerade die HTTP-Schicht oder den Datenbankzugriff sehen will, ohne die Zeilen wieder auszukommentieren.

Wer sich einmal mit dem [Ereignismodell von Node.js](https://www.mikebild.dev/de/blog/nodejs-eventemitter-und-ereignismodell/) beschäftigt hat, kennt das Problem in verschärfter Form: Bei asynchronem Code ist die Reihenfolge der Ausgaben nicht immer die, die man erwartet, und ein `console.log` mitten in einem Callback erzählt nur einen Bruchteil der Geschichte. Genau hier lohnt sich der Schritt zu Werkzeugen, die den laufenden Prozess anhalten und untersuchen können.

## Der eingebaute Inspector: --inspect und --inspect-brk

Das Wichtigste zuerst, weil es in Teams immer wieder für Überraschung sorgt: Der Debugger steckt bereits in Node.js. Es gibt kein Extra-Paket zu installieren. Historisch gab es einmal ein separates Modul (`node-inspector` mit dem Befehl `node-debug app.js`), aber das ist seit Node 6 überholt und heute nur noch eine Randnotiz. Seit Node 6.3 spricht der Prozess selbst das Chrome DevTools Protocol über einen WebSocket – und genau darum funktionieren sowohl die Chrome DevTools als auch VS Code als Client.

Gestartet wird das mit einem Flag:

```bash
node --inspect ./src/server.js
```

Auf stderr erscheint dann eine Zeile wie diese:

```text
Debugger listening on ws://127.0.0.1:9229/8f2c...-uuid
For help, see: https://nodejs.org/en/docs/inspector
```

Der Prozess bindet standardmäßig an `127.0.0.1:9229`. Wichtig ist der Unterschied zwischen den beiden zentralen Flags:

- `--inspect` startet nur den Debug-Server, der Code läuft ganz normal weiter. Das nutze ich bei einem Server, der ohnehin dauerhaft läuft und auf Anfragen wartet.
- `--inspect-brk` startet ebenfalls den Debug-Server, hält aber vor der ersten Zeile meines eigenen Codes an. Das ist ideal für kurz laufende Skripte, die sonst durchgerauscht wären, bevor ich überhaupt einen Client verbinden konnte.

```bash
node --inspect-brk ./src/server.js
```

Für ein Skript, das nach 200 Millisekunden fertig ist, ist `--inspect-brk` praktisch die einzige sinnvolle Wahl – mit `--inspect` allein wäre der Prozess längst beendet, bevor ich in den DevTools auf "connect" geklickt habe. Diesen Unterschied zu kennen hat mir in der Praxis schon viel Zeit gespart.

Wer den Inspector nicht beim Start, sondern erst bei einem bereits laufenden Prozess aktivieren will, kann unter Unix-Systemen das Signal `SIGUSR1` schicken. Unter Windows steht dieser Weg nicht zur Verfügung.

## Vom Flag zum Client

Der offene Inspector-Port ist nur die halbe Miete – ich brauche noch einen Client, der sich verbindet. Zwei Wege nutze ich regelmäßig.

In Chrome (oder Edge) öffne ich `chrome://inspect` beziehungsweise `edge://inspect`. Unter "Remote Target" taucht der laufende Node-Prozess auf, ein Klick auf "inspect" öffnet die vertrauten DevTools mit Sources, Breakpoints, Call Stack und Konsole. Über "Configure" lassen sich Host und Port anpassen, falls ich nicht die Standardwerte verwende.

In VS Code lege ich mir eine Attach-Konfiguration in der `.vscode/launch.json` an:

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Node",
      "port": 9229
    }
  ]
}
```

Damit hänge ich mich an einen Prozess, den ich vorher mit `--inspect` gestartet habe. Alternativ gibt es in VS Code den "Auto Attach"-Modus, der sich automatisch an Node-Prozesse hängt, die aus dem integrierten Terminal gestartet werden. Beide Wege enden an derselben Stelle: interaktiven Breakpoints, an denen ich Variablen inspizieren und mich Schritt für Schritt durch den Code bewegen kann.

Der folgende Ablauf fasst zusammen, wie Flag, Agent und Client zusammenspielen:

```mermaid
flowchart LR
  A["node --inspect<br/>oder --inspect-brk"] --> B["Inspector-Agent<br/>CDP über ws://127.0.0.1:9229"]
  B --> C["Chrome DevTools<br/>chrome://inspect"]
  B --> D["VS Code<br/>request: attach"]
  C --> E["Breakpoints<br/>CPU-Profil<br/>Heap-Snapshot"]
  D --> E
```

Beide Clients reden dasselbe Protokoll mit demselben eingebauten Agenten. Ich muss mich also nicht für ein Ökosystem entscheiden – ich wähle den Client, der mir gerade besser passt.

## Das debug-Modul: schaltbares Logging ohne Codeänderung

Nicht jedes Problem verdient einen Breakpoint. Oft will ich nur nachvollziehen, welchen Weg eine Anfrage durch meine Schichten nimmt – aber eben nur dann, wenn ich es gerade untersuche, und ohne die Ausgabe dauerhaft im Code zu verankern. Genau dafür gibt es das `debug`-Modul, zum Stichtag in Version 4.3.4. Viele bekannte Bibliotheken nutzen es intern, und für eigenen Code ist es schnell eingebaut.

Der Kern ist eine Factory, die man mit einem Namespace aufruft:

```js
const debug = require('debug')('app:http');

function handleRequest(req, res) {
  debug('incoming request %s %s', req.method, req.url);
  // ... handle the request ...
}
```

Der Clou steckt nicht im Code, sondern im Start. Standardmäßig gibt diese Zeile nichts aus. Erst wenn ich die Umgebungsvariable `DEBUG` setze, wird der passende Namespace sichtbar:

```bash
DEBUG=app:* node server.js
```

Das `*` ist eine Wildcard, und mit einem führenden Minus schließe ich Namespaces gezielt aus. So sehe ich zum Beispiel alles aus `app`, aber nicht die gesprächige Datenbankschicht:

```bash
DEBUG=app:*,-app:db node server.js
```

Damit ist der zentrale Vorteil erreicht: Ich schalte Logging an und aus, ohne eine einzige Zeile Code zu ändern. Das `debug`-Modul bringt außerdem automatische Zeitdifferenzen zwischen den Ausgaben mit (die `+0ms`-Angaben), färbt bei einem echten Terminal die Namespaces ein und kennt Format-Platzhalter wie `%s`, `%d`, `%j`, `%o` und `%O`.

Ein Detail sorgt regelmäßig für Verwirrung: Die Ausgabe von `debug` geht auf **stderr**, nicht auf stdout. Wer die Ausgabe umleiten oder mit `grep` filtern will und dabei nur stdout betrachtet, sieht schlicht nichts. Und `DEBUG` greift nur für Bibliotheken, die selbst `debug` verwenden – es ist kein globaler Schalter für jede beliebige Log-Ausgabe.

## nodemon: Auto-Reload in der Entwicklung

Ein Werkzeug, das streng genommen nicht zum Debuggen gehört, aber die Feedback-Schleife enorm verkürzt, ist nodemon (zum Stichtag rund Version 2.0.20). Es beobachtet Dateien und startet den Prozess bei jeder Änderung neu, sodass ich nicht ständig von Hand `Strg+C` und neu starten muss. Ich nehme es als Dev-Abhängigkeit ins Projekt:

```bash
nodemon --watch ./ --ignore ./node_modules ./bin/www
```

Schöner ist eine `nodemon.json` im Projektwurzelverzeichnis, weil sie die Konfiguration festhält und sich gut mit dem Inspector kombinieren lässt:

```json
{
  "watch": ["src"],
  "ext": "js,json",
  "ignore": ["node_modules"],
  "exec": "node --inspect src/server.js"
}
```

Über `exec` starte ich den Prozess direkt mit `--inspect`, sodass ich Auto-Reload und Debugger zusammen bekomme. Node.js selbst brachte zum Stichtag zwar bereits einen experimentellen Watch-Modus mit, aber für den Alltag setze ich weiterhin auf nodemon, weil es ausgereift und flexibel konfigurierbar ist.

## Ein schneller Blick auf den Speicher

Bevor ich zum eigentlichen Profiling komme, gibt es einen sehr günstigen Zwischenschritt, der ohne jedes Werkzeug auskommt: `process.memoryUsage()`. Diese Funktion liefert ein Objekt mit mehreren Werten, alle in Bytes:

```js
const { rss, heapTotal, heapUsed, external } = process.memoryUsage();

console.error(
  `rss=${(rss / 1e6).toFixed(1)}MB ` +
  `heapUsed=${(heapUsed / 1e6).toFixed(1)}MB ` +
  `heapTotal=${(heapTotal / 1e6).toFixed(1)}MB`
);
```

Die wichtigsten Felder in der Praxis: `heapUsed` und `heapTotal` betreffen den von V8 verwalteten JavaScript-Heap, während `rss` (Resident Set Size) den gesamten Speicher des Prozesses angibt, inklusive nativer Anteile. Wenn ich in einer Schleife oder über mehrere Anfragen hinweg zusehe, wie `heapUsed` immer weiter steigt und nach der Garbage Collection nicht mehr zurückgeht, habe ich einen ersten, sehr konkreten Hinweis auf ein Speicherleck. Für einen schnellen RSS-Wert allein gibt es seit Node 15.6 zusätzlich `process.memoryUsage.rss()`.

Das ersetzt kein richtiges Profiling, aber es ist ein billiger Frühwarnwert – gerade bei langlaufenden Prozessen oder bei Verarbeitung großer Datenmengen, wie sie beim Umgang mit [Streams und Backpressure](https://www.mikebild.dev/de/blog/nodejs-streams-und-backpressure/) auftreten.

## Profiling über denselben Inspector

Wenn der grobe Blick mit `process.memoryUsage()` nicht reicht, führt der nächste Schritt zurück zum Inspector. Dieselbe Verbindung, die ich zum Setzen von Breakpoints nutze, öffnet in den DevTools auch die Tabs für Analyse: Unter "Memory" nehme ich einen Heap-Snapshot auf und sehe, welche Objekte wie viel Speicher belegen. Unter "Profiler" beziehungsweise "Performance" zeichne ich ein CPU-Profil auf und finde heraus, in welchen Funktionen die Zeit tatsächlich verbraucht wird.

Wer lieber ohne grafisches Frontend arbeitet, hat CLI-Alternativen. `node --prof` schreibt ein V8-Log, das ich anschließend mit `node --prof-process` in einen lesbaren Bericht verwandle:

```bash
node --prof ./src/server.js
# ... generate load, then stop the process ...
node --prof-process isolate-0x*.log > processed.txt
```

Daneben gibt es seit Node 12 die noch experimentellen Flags `--cpu-prof` und `--heap-prof`, die direkt Profildateien im DevTools-Format erzeugen. Im Team hat sich trotzdem meist der interaktive Weg über die DevTools durchgesetzt, weil er dieselbe Oberfläche nutzt, die aus dem Frontend ohnehin vertraut ist.

## Die vier Stufen im Überblick

Zurück zur Eskalationsleiter vom Anfang. Ich fasse die vier Werkzeuge bewusst als Stufen zusammen, weil jede ihren eigenen Zweck hat und keine die andere vollständig ersetzt:

- `console.log` – immer aktiv, unstrukturiert, sofort verfügbar. Gut für den schnellen Einzelblick, schlecht als Dauerlösung und in Produktion fehl am Platz.
- `debug` – namespacegesteuert, über die `DEBUG`-Umgebungsvariable ohne Codeänderung schaltbar, Ausgabe auf stderr. Ideal, um in der Entwicklung gezielt Schichten sichtbar zu machen.
- Inspector – interaktive Breakpoints in Chrome DevTools oder VS Code, über `--inspect` beziehungsweise `--inspect-brk`. Der Schritt vom Vermuten zum Untersuchen.
- Profiler – derselbe Inspector, aber für CPU-Profile und Heap-Snapshots. Kommt zum Einsatz, wenn es nicht um Korrektheit, sondern um Performance und Speicher geht.

```mermaid
flowchart TB
  A["console.log<br/>immer an, unstrukturiert"] --> B["debug<br/>DEBUG-Env, namespacegesteuert"]
  B --> C["Inspector<br/>interaktive Breakpoints"]
  C --> D["Profiler<br/>CPU-Profil und Heap-Snapshot"]
```

## Zwei Fallstricke, die teuer werden können

Zum Schluss zwei Dinge, die ich in Code-Reviews ausdrücklich anspreche, weil sie in Produktion echten Schaden anrichten.

Der erste betrifft `console.log` in Produktion. Was in der Entwicklung harmlos aussieht, wird im Dauerbetrieb zum Problem: Die Ausgabe ist unstrukturiert, lässt sich nicht per Schalter abstellen, verschmutzt stdout und kostet je nach Menge Performance. Für die Entwicklung ist `debug` die bessere Wahl, und für Produktion gehört ein richtiger Logger her, der Log-Level, strukturierte Ausgabe und gezieltes Ein- und Ausschalten beherrscht.

Der zweite Fallstrick ist gefährlicher und wird oft unterschätzt: einen offenen `--inspect`-Port stehen zu lassen oder ihn an `0.0.0.0` zu binden. Der Debug-Port erlaubt das Ausführen beliebigen Codes im Prozess – wer ihn erreicht, kann den Server übernehmen. Deshalb bindet Node.js von Haus aus nur an `127.0.0.1`, und dabei sollte es in aller Regel bleiben. Muss ich wirklich einen entfernten Prozess debuggen, tunnele ich den Port über SSH statt ihn öffentlich zu exponieren:

```bash
ssh -L 9229:localhost:9229 user@remote-host
```

Und das Debug-Flag hat in keinem Produktions-Startskript etwas verloren. Diese Trennung zwischen Entwicklungs- und Produktionsbetrieb hängt eng mit dem zusammen, was schon im [Node.js-Training zur Event-Loop-Architektur](https://www.mikebild.dev/de/blog/nodejs-training-event-loop-architektur/) zur Sprache kommt: Werkzeuge, die in den laufenden Prozess greifen, sind mächtig – und genau deshalb muss der Zugang zu ihnen eng bleiben.

## Ein Ausblick

Wer noch tiefer gehen will, findet in Node.js zwei fortgeschrittene Bausteine, die ich hier nur als Ausblick nenne, weil sie eigene Beiträge verdienen: `async_hooks`, um den Lebenszyklus asynchroner Ressourcen zu verfolgen, und `diagnostics_channel`, um strukturierte Diagnose-Ereignisse zwischen Bibliotheken und Beobachtern auszutauschen. Beide sind eher etwas für Werkzeugbauer und Bibliotheksautoren als für den täglichen Fehlerfang.

## Fazit

Der Weg vom `console.log`-Reflex zu bewusstem Debuggen ist kein großer Sprung, sondern eine kleine Leiter mit vier Sprossen. Für den schnellen Blick bleibt `console.log`, aber sobald es strukturierter wird, übernimmt `debug` mit seinem schaltbaren Logging. Wird es wirklich knifflig, hält der eingebaute Inspector den Prozess an und lässt mich Variablen und Aufrufstapel untersuchen – wahlweise in Chrome DevTools oder VS Code, denn beide sprechen dasselbe Protokoll. Und wenn Performance oder Speicher das Thema sind, wechselt derselbe Inspector in den Profiler-Modus, während `process.memoryUsage()` schon vorher einen billigen Frühwarnwert liefert. Wer diese Stufen kennt und den Debug-Port konsequent verschlossen hält, hört auf zu raten und fängt an zu untersuchen.

## Weiterführende Quellen

- Grounding aus dem Repository: `introduction-nodejs/19-quality-tools.md` (Kapitel "Quality Tools")
- Debugging Node.js (offizieller Guide): https://nodejs.org/en/learn/getting-started/debugging
- CLI-Optionen (`--inspect`, `--inspect-brk`, `--prof`, `--cpu-prof`, `--heap-prof`): https://nodejs.org/api/cli.html
- Inspector-Modul: https://nodejs.org/api/inspector.html
- `process.memoryUsage()`: https://nodejs.org/api/process.html#processmemoryusage
- debug-Modul: https://www.npmjs.com/package/debug bzw. https://github.com/debug-js/debug
- nodemon: https://github.com/remy/nodemon
- Ausblick – diagnostics_channel: https://nodejs.org/api/diagnostics_channel.html · async_hooks: https://nodejs.org/api/async_hooks.html
