# EventEmitter: das Ereignismodell im Kern von Node.js

URL: https://www.mikebild.dev/de/blog/nodejs-eventemitter-und-ereignismodell/

Dass `.on('data', …)` an einem Stream, `.on('request', …)` an einem HTTP-Server und `.on('exit', …)` an einem Kindprozess gleich aussehen, ist keine Konvention, an die sich die Modulautoren freiwillig halten. Die Ähnlichkeit hat einen technischen Grund: Diese Objekte erben alle von derselben Klasse – dem `EventEmitter`. Wer diese eine Klasse durchdrungen hat, versteht damit einen großen Teil der Standardbibliothek.

Dieser Artikel nimmt den `EventEmitter` auseinander: erst die Idee dahinter, dann die konkrete API, dann die zwei, drei Stellen, an denen es in der Praxis wehtut. Wer vorher noch einmal nachlesen will, warum Node überhaupt ereignisgetrieben arbeitet, findet das im Beitrag zum [Node.js-Training rund um den Event Loop](https://www.mikebild.dev/de/blog/nodejs-training-event-loop-architektur/). Hier geht es um die Schicht direkt darüber.

## Der gemeinsame Nenner unter allem

Node.js ist im Kern ereignisgetrieben. Ein HTTP-Server wartet nicht in einer Schleife auf die nächste Anfrage, sondern meldet an: "Wenn eine Anfrage kommt, ruf diese Funktion." Ein Lesestrom sagt nicht "gib mir die Daten", sondern "sag mir Bescheid, wenn ein Datenstück da ist". Dieses Muster – Interesse anmelden, später benachrichtigt werden – zieht sich durch die gesamte Plattform.

Damit dieses Muster nicht in jedem Modul neu erfunden wird, gibt es eine gemeinsame Basisklasse. `net.Server`, `http.Server`, die Stream-Klassen `Readable` und `Writable`, das globale `process`-Objekt, die Objekte aus `child_process` – sie alle sind Emitter oder erben von `EventEmitter`. Das erklärt, warum sich `.on()` überall gleich anfühlt: Es ist buchstäblich dieselbe Methode.

```mermaid
classDiagram
  class EventEmitter {
    +on(event, listener)
    +once(event, listener)
    +off(event, listener)
    +emit(event, ...args)
  }
  EventEmitter <|-- HttpServer
  EventEmitter <|-- Readable
  EventEmitter <|-- Writable
  EventEmitter <|-- Process
  EventEmitter <|-- MyEmitter
  class HttpServer["http.Server"]
  class Readable["stream.Readable"]
  class Writable["stream.Writable"]
  class Process["process"]
  class MyEmitter["eigene Klasse"]
```

Die Klasse selbst liegt im eingebauten Modul `events`. Man erreicht sie klassisch über `require('events')` oder – seit Node 16 möglich – über den Prefix `require('node:events')`. Beide Schreibweisen liefern dieselbe Klasse; der Prefix macht nur unmissverständlich klar, dass ein eingebautes Modul gemeint ist und kein Paket aus `node_modules`.

## Die vier Methoden, um die sich alles dreht

Die API des Emitters ist erstaunlich klein. Vier Methoden reichen für den Alltag. Drei davon – `on`, `once` und `off` – geben den Emitter selbst zurück und sind damit verkettbar; `emit` bildet die Ausnahme und liefert stattdessen einen `boolean`, wie wir gleich sehen werden.

```js
const { EventEmitter } = require('node:events');

const emitter = new EventEmitter();

function onTask(job) {
  console.log('received task', job.id);
}

emitter.on('task', onTask);        // register a listener
emitter.emit('task', { id: 1 });   // trigger it synchronously
emitter.off('task', onTask);       // remove it again
```

`on(eventName, listener)` hängt einen Listener ans Ende der Liste für dieses Event. `emit(eventName, ...args)` ruft alle registrierten Listener auf und reicht die zusätzlichen Argumente durch. `off(eventName, listener)` – Alias `removeListener` – nimmt einen Listener wieder heraus. Und `once(eventName, listener)` registriert einen Listener, der nach dem ersten Auslösen automatisch verschwindet.

Zwei Eigenschaften von `emit` sind für das Verständnis entscheidend, und beide werden gern übersehen:

- `emit` ruft die Listener **synchron** auf. Es gibt hier nichts Asynchrones, keinen Umweg über den Event Loop, keine Verzögerung. Wenn `emit` zurückkehrt, sind alle Listener durchgelaufen.
- Die Listener laufen **in der Reihenfolge ihrer Registrierung**. Wer zuerst `on()` aufgerufen hat, dessen Funktion läuft zuerst.

```mermaid
sequenceDiagram
  participant C as Aufrufer
  participant E as EventEmitter
  participant L1 as Listener 1
  participant L2 as Listener 2
  C->>E: emit('task', data)
  E->>L1: listener(data)
  L1-->>E: fertig
  E->>L2: listener(data)
  L2-->>E: fertig
  E-->>C: return true
```

Der Rückgabewert von `emit` ist ein `boolean`: `true`, wenn es für dieses Event mindestens einen Listener gab, sonst `false`. Was die Listener selbst zurückgeben, interessiert den Emitter nicht – Rückgabewerte von Listenern werden schlicht ignoriert. Wer aus einem Listener heraus etwas nach draußen melden will, muss das über einen anderen Weg tun, etwa ein weiteres Event oder einen geteilten Zustand.

## on, once und off im Zusammenspiel

Der Unterschied zwischen `on` und `once` klingt harmlos, hat aber Konsequenzen. Ein mit `once` registrierter Listener wird **vor** seinem eigenen Aufruf aus der Liste entfernt und dann ausgeführt. Er feuert also garantiert genau einmal, egal wie oft das Event danach noch ausgelöst wird.

```js
const { EventEmitter } = require('node:events');
const queue = new EventEmitter();

let handled = 0;
queue.on('task', () => { handled += 1; });

let drained = 0;
queue.once('drain', () => { drained += 1; });

queue.emit('task');   // handled = 1
queue.emit('task');   // handled = 2
queue.emit('drain');  // drained = 1
queue.emit('drain');  // drained stays 1 - the listener is gone
```

Beim Entfernen lohnt ein genauer Blick. `off` entfernt **höchstens eine** Instanz eines Listeners. Wenn man dieselbe Funktion versehentlich zweimal mit `on` registriert hat, braucht man auch zwei `off`-Aufrufe, um sie vollständig loszuwerden. Und man kann nur entfernen, was man auch benennen kann – eine anonyme Arrow-Function, die man direkt an `on` übergibt, hat man nachher nicht mehr in der Hand. Deshalb registriere ich Listener, die ich später wieder abmelden will, immer als benannte Funktion oder als Variable.

Hier die Semantik der vier auf einen Blick:

- `on` – Listener bleibt registriert und läuft bei jedem `emit` erneut.
- `once` – Listener läuft genau einmal und entfernt sich vorher selbst.
- `off` – entfernt eine Instanz eines benannten Listeners; anonyme Funktionen bekommt man so nicht mehr weg.
- `emit` – ruft alle Listener synchron in Registrierungsreihenfolge auf und liefert `true`/`false` zurück.

Neben diesen vieren gibt es noch nützliche Randmethoden: `prependListener` hängt einen Listener an den Anfang statt ans Ende, `removeAllListeners` räumt komplett auf, `listeners(eventName)` gibt eine Kopie der aktuellen Listener-Liste zurück und `eventNames()` verrät, für welche Events überhaupt jemand registriert ist. Man braucht sie selten, aber wenn man sie braucht, sind sie Gold wert – etwa beim Debuggen der Frage "wer hört hier eigentlich alles zu?".

## Das error-Event ist ein Sonderfall

Diese Eigenheit hat in einem Projekt einmal einen laufenden Prozess gekostet: Das Event `'error'` verhält sich anders als alle anderen. Bei einem gewöhnlichen Event ohne Listener passiert nichts – `emit` liefert `false` und gut ist. Beim `'error'`-Event ist das nicht so.

Löst ein Emitter `emit('error', ...)` aus und es ist **kein** Listener für `'error'` registriert, dann wird der Fehler geworfen. Und weil er innerhalb der Emitter-Maschinerie geworfen wird und niemand ihn auffängt, reißt er im Zweifel den ganzen Prozess ab. Aus einem "ein Stream hat sich beschwert" wird so ein kompletter Absturz des Node-Prozesses.

```js
const { EventEmitter } = require('node:events');
const source = new EventEmitter();

// Without this listener the next line would crash the process:
source.on('error', (err) => {
  console.error('handled:', err.message);
});

source.emit('error', new Error('boom'));  // handled cleanly
```

Das ist übrigens keine Schikane, sondern eine bewusste Entscheidung des Node-Teams. Ein Fehler, den niemand behandelt, soll laut sein und nicht still verschluckt werden. Daraus folgt eine einfache Regel: Für jeden Emitter, der Fehler melden kann – und das sind praktisch alle Streams, alle Netzwerkobjekte, alle Kindprozesse – registriere ich einen `'error'`-Listener, bevor die eigentliche Arbeit losgeht. Er ist das Auffangnetz, ohne das die ganze Anwendung an einer einzigen unbehandelten Meldung zerbricht.

Es gibt noch zwei weitere Spezial-Events, die intern verwendet werden: `'newListener'` feuert, bevor ein neuer Listener hinzugefügt wird, und `'removeListener'` feuert, nachdem einer entfernt wurde. Beide sind eher für Werkzeuge und Diagnose gedacht als für den Alltag, aber es ist gut zu wissen, dass es sie gibt, wenn man einmal beobachten will, wer sich wann an- oder abmeldet.

## Zu viele Listener: die Leck-Warnung

Node führt für jeden Emitter Buch darüber, wie viele Listener pro Event registriert sind. Überschreitet diese Zahl einen Schwellenwert – standardmäßig 10 –, gibt Node eine Warnung auf stderr aus: die `MaxListenersExceededWarning`.

Diese Warnung ist keine Fehlermeldung und stoppt nichts. Sie ist eine Heuristik gegen eine sehr häufige Fehlerklasse: das Listener-Leck. Der typische Fall sieht so aus, dass irgendwo pro Anfrage, pro Schleifendurchlauf oder pro Wiederholung ein `on()` aufgerufen wird, das dazugehörige `off()` aber fehlt. Die Listener stapeln sich, der Speicher wächst, und irgendwann meldet sich Node.

```js
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();

// Optional: make the warning itself visible
process.on('warning', (warning) => {
  console.log(warning.name, '- count:', warning.count);
});

// Anti-pattern: a new, never-removed listener on every iteration
for (let i = 0; i < 12; i += 1) {
  bus.on('data', (chunk) => processChunk(chunk));
}
// From the 11th listener on, the MaxListenersExceededWarning appears.
```

Entscheidend ist: Die Warnung ist das Symptom, nicht die Ursache. Die Ursache ist die nie entfernte Registrierung. Wer die Warnung mit `emitter.setMaxListeners(Infinity)` zum Schweigen bringt, hat nur das Warnlämpchen abgeklebt – das Leck läuft weiter, der Speicher wächst weiter. Richtig ist, die Listener wieder abzumelden, wenn man sie nicht mehr braucht, oder sie gleich als `once` zu registrieren, wenn sie ohnehin nur einmal feuern sollen.

```js
function onData(chunk) {
  processChunk(chunk);
}

bus.on('data', onData);
// ... later, when done:
bus.off('data', onData);   // cleanly removed, no pile-up
```

Das heißt nicht, dass der Schwellenwert von 10 in Stein gemeißelt ist. Es gibt legitime Fälle, in denen ein Emitter bewusst mehr Listener hat. Dann justiert man das absichtlich: `emitter.setMaxListeners(n)` hebt das Limit für einen einzelnen Emitter an, `events.defaultMaxListeners = n` verschiebt den globalen Standardwert. Der Wert `0` beziehungsweise `Infinity` schaltet die Warnung ganz ab. Wichtig ist nur, dass diese Entscheidung eine bewusste ist und kein Reflex, um die Meldung im Log loszuwerden.

## Eine eigene Klasse bauen

Der `EventEmitter` ist nicht nur etwas, von dem die Standardbibliothek erbt – man kann ihn genauso gut selbst als Basis nehmen. Immer dann, wenn eine eigene Komponente ihre Umgebung über Dinge informieren soll, ohne die Empfänger fest zu kennen, ist ein Emitter die naheliegende Wahl.

Das Muster ist `class MyThing extends EventEmitter` mit einem `super()`-Aufruf im Konstruktor. Hier eine kleine Aufgabenwarteschlange, die meldet, wenn eine Aufgabe hinzukommt, wenn sie abgearbeitet ist und wenn etwas schiefgeht:

```js
const { EventEmitter } = require('node:events');

class TaskQueue extends EventEmitter {
  constructor() {
    super();
    this.pending = [];
  }

  add(job) {
    this.pending.push(job);
    this.emit('task', job);
  }

  run() {
    while (this.pending.length > 0) {
      const job = this.pending.shift();
      try {
        job.handler();
        this.emit('done', job);
      } catch (cause) {
        // Only emit 'error' when a listener exists -
        // otherwise it crashes the process:
        if (this.listenerCount('error') > 0) {
          this.emit('error', cause);
        } else {
          throw cause;
        }
      }
    }
    this.emit('drain');
  }
}

const queue = new TaskQueue();

queue.on('task', (job) => console.log('queued', job.id));
queue.on('done', (job) => console.log('done', job.id));
queue.once('drain', () => console.log('all work finished'));
queue.on('error', (err) => console.error('failed:', err.message));

queue.add({ id: 1, handler: () => {} });
queue.add({ id: 2, handler: () => { throw new Error('bad job'); } });
queue.run();
```

An diesem Beispiel sieht man mehrere Dinge zusammenwirken. Die Warteschlange kennt ihre Listener nicht – sie ruft `emit` auf und wer will, hört zu. `once('drain', ...)` sorgt dafür, dass die Abschlussmeldung genau einmal kommt. Und beim Fehlerfall prüfe ich mit `listenerCount('error')`, ob überhaupt jemand zuhört, bevor ich `emit('error', ...)` aufrufe. So vermeide ich den Absturz durch ein unbehandeltes `'error'`-Event und lasse den Fehler stattdessen als gewöhnliche Exception weiterlaufen, wenn niemand ihn abfängt.

Ein kleiner, aber wiederkehrender Stolperstein bei eigenen Klassen betrifft `this`. In einem klassischen Function-Listener zeigt `this` auf den Emitter, der das Event ausgelöst hat – praktisch, wenn man von innen auf den Emitter zugreifen will. In einer Arrow-Function gilt das nicht; dort behält `this` seine Bedeutung aus dem umgebenden Kontext. Beides ist legitim, man muss sich nur bewusst sein, welche Variante man wählt.

## Synchron heißt synchron

Zum Schluss noch eine Klarstellung, die im Alltag Fehler verhindert. Weil Node so stark mit Asynchronität verbunden ist, nehmen viele intuitiv an, `emit` würde die Listener irgendwann später aufrufen. Das tut es nicht. Der Aufruf ist synchron, und die Listener blockieren den aufrufenden Code, bis sie durch sind.

Das ist meistens genau richtig, denn es macht den Ablauf vorhersehbar. Wer aber innerhalb eines Listeners bewusst etwas aus dem aktuellen Ablauf herauslösen will – damit lange Arbeit den `emit`-Aufrufer nicht aufhält –, muss das ausdrücklich tun, etwa mit `setImmediate` oder `process.nextTick`. Der Emitter selbst plant nichts um; er ruft nur der Reihe nach auf, was registriert ist.

## Fazit

Der `EventEmitter` ist eine kleine Klasse mit großer Reichweite. Vier Methoden – `on`, `once`, `off`, `emit` – tragen den Alltag, und weil ein großer Teil der Standardbibliothek von genau dieser Klasse erbt, überträgt sich das Verständnis unmittelbar auf Streams, Server und Prozesse. Zwei Dinge sollte man dabei nie aus den Augen verlieren: Das `'error'`-Event stürzt ohne Listener den Prozess ab, deshalb gehört überall dort, wo Fehler gemeldet werden können, ein `'error'`-Listener hin. Und die `MaxListenersExceededWarning` ist ein ehrliches Warnsignal für ein Leck, kein lästiger Hinweis, den man wegkonfiguriert. Mit diesen beiden Reflexen schreibt man ereignisgetriebenen Node-Code, der auch dann noch stabil läuft, wenn es ungemütlich wird.

## Weiterführende Quellen

- Node.js-Doku: [Events-API](https://nodejs.org/docs/latest-v16.x/api/events.html)
- Node.js-Guide: [The Node.js Event emitter](https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter)
- Node.js-Doku: [Stream-API](https://nodejs.org/docs/latest-v16.x/api/stream.html) (erbt von `EventEmitter`)
- Eigener Beitrag: [Node.js-Training rund um den Event Loop](https://www.mikebild.dev/de/blog/nodejs-training-event-loop-architektur/)
