# Control Flow vor `async/await`

URL: https://www.mikebild.dev/de/blog/control-flow-vor-async-await/

Node 0.10 kennt Funktionen, Callbacks und einen Event Loop. Was es nicht kennt, ist ein Befehl, der sagt: „Mach das, warte auf das Ergebnis, mach dann das Nächste.“ Diese Reihenfolge muss man selbst herstellen, Schritt für Schritt, von Hand. Und sobald drei oder vier Schritte aufeinander aufbauen, wird sichtbar, dass „warten“ in einer Single-Thread-Umgebung kein Sprachfeature ist, sondern eine Entwurfsaufgabe.

Ich habe mir dafür eine kleine Bibliothek gebaut, `cflow`. Sie ist so klein, dass man sie in einem Rutsch liest, und genau deshalb legt sie den Unterschied offen: zwischen einer verschachtelten Callback-Kette und einer Pipeline, deren Übergänge man einmal modelliert hat.

## Das Problem: Reihenfolge aus Callbacks bauen

Eine asynchrone Operation in Node folgt einer Konvention. Sie nimmt als letztes Argument einen Callback, und dieser Callback bekommt als erstes Argument einen möglichen Fehler. Das nennt man error-first oder node-style callback:

```js
fs.readFile("config.json", function (err, data) {
  if (err) return handle(err);
  // continue with data
});
```

Das ist für einen Schritt unauffällig. Es wird unangenehm, sobald der nächste Schritt das Ergebnis des vorherigen braucht. Dann landet der zweite Aufruf im Callback des ersten, der dritte im Callback des zweiten, und so weiter:

```js
loadUser(id, function (err, user) {
  if (err) return done(err);
  loadAccount(user.accountId, function (err, account) {
    if (err) return done(err);
    loadInvoices(account.id, function (err, invoices) {
      if (err) return done(err);
      done(null, summarize(user, account, invoices));
    });
  });
});
```

Diese Form hat einen festen Namen bekommen: Pyramid of Doom, manchmal auch Callback-Hölle. Der Code wandert nach rechts, die schließenden Klammern stapeln sich, und die eigentliche Absicht – drei Dinge nacheinander laden und zusammenfassen – verschwindet hinter der Mechanik. Das ist nicht nur ein optisches Ärgernis. Jede Ebene wiederholt dieselbe Fehlerbehandlung, und genau dort schleichen sich Fehler ein.

## Der Fehlerfall, den man fast immer einmal hat

Die error-first-Konvention verlangt eine Disziplin, die das Sprachmittel selbst nicht erzwingt: nach `if (err)` muss ein `return` stehen. Fehlt es, läuft die Funktion weiter:

```js
loadUser(id, function (err, user) {
  if (err) done(err);            // Careful: no return
  loadAccount(user.accountId, function (err, account) {
    // ...
  });
});
```

Im Fehlerfall passiert hier zweierlei gleichzeitig. `done(err)` wird aufgerufen, der Aufrufer hält das also für erledigt und meldet einen Fehler. Im selben Atemzug greift `user.accountId` auf ein `user` zu, das `undefined` ist, weil ja ein Fehler vorlag. Das wirft eine zweite, ganz andere Ausnahme, oder `done` wird ein zweites Mal aufgerufen – diesmal mit einem irreführenden Ergebnis. Ein Callback, der doppelt feuert, ist besonders gemein, weil die Reihenfolge der Meldungen nicht mehr garantiert ist. Mal kommt erst der echte Fehler, mal erst der Folgefehler. Solche Bugs sind im Log schwer zu lesen, weil die Ursache und ihre Auswirkung zeitlich auseinanderfallen.

Daraus folgt nicht, dass Callbacks schlecht wären. Der Befund ist ein anderer: Wenn jeder Schritt seine Fehlerprüfung und seinen Übergang zum nächsten Schritt selbst von Hand schreibt, dann ist jede dieser Stellen eine Gelegenheit, es falsch zu machen. Was fehlt, ist eine Stelle, die den Kontrollfluss einmal modelliert, statt ihn an jeder Verschachtelung neu zu erfinden.

## Bibliotheken, die den Fluss übernehmen

Damit ist man im Jahr 2014 nicht allein. Caolan McMahons `async` ist der etablierte Werkzeugkasten und bietet genau für diese Muster benannte Funktionen an. `async.series` führt Aufgaben nacheinander aus, `async.parallel` gleichzeitig, und `async.waterfall` reicht das Ergebnis jedes Schritts an den nächsten weiter:

```js
async.waterfall([
  function (next) { loadUser(id, next); },
  function (user, next) { loadAccount(user.accountId, next); },
  function (account, next) { loadInvoices(account.id, next); }
], function (err, invoices) {
  if (err) return handle(err);
  // done
});
```

Das ist flach statt verschachtelt, und es gibt nur noch einen Fehlerausgang. Parallel dazu reift ein zweiter Ansatz heran: Promises. Die Promises/A+-Spezifikation hat sich durchgesetzt, Bibliotheken wie Q, when.js und bluebird sind verbreitet, und man kann Ketten mit `.then(...)` schreiben. Ein eingebautes `Promise` gibt es in der stabilen Node-Version 0.10 allerdings noch nicht zuverlässig; das kommt erst mit der V8-Aktualisierung der 0.11er-Linie. Wer Promises will, bindet sie heute als Abhängigkeit ein.

## cflow: der Kontrollfluss auf das Nötigste reduziert

`cflow` geht den waterfall-Weg, aber radikal klein. Die gesamte Bibliothek ist eine einzige Funktion:

```js
var Flow = function () {
  this.args = Array.prototype.slice.call(arguments);
  var next = function () {
    if (this.args.length > 0) {
      this.args.shift();
      this.args[0].apply(next, Array.prototype.slice.call(arguments));
    }
  }.bind(this);
  this.args[0].apply(next);
};
```

Der Trick steckt in zwei Zeilen. `Flow` bekommt eine Liste von Funktionen. Jede dieser Funktionen wird mit `this` aufgerufen, das auf `next` gebunden ist – also auf die Fortsetzung. Ein Schritt ruft am Ende seiner Arbeit einfach `this(...)` auf. Das verwirft den aktuellen Schritt (`shift`) und ruft den nächsten mit genau den Argumenten auf, die man `this` übergeben hat. Die Werte fließen so von Schritt zu Schritt weiter.

In der Anwendung sieht das aus wie eine gerade Liste von Phasen statt wie ein Pyramidenbau:

```js
var Flow = require("cflow");

new Flow(
  function () {
    slow("A", this);                 // this is the continuation
  },
  function (data) {
    slow2(data + "B", "!!!", this);  // data is "A"
  },
  function (data, data2) {
    slow(data + "C" + data2, this);  // data is "AB", data2 is "!!!"
  },
  function (data) {
    console.log(data);               // "ABC!!!"
  }
);

function slow(value, callback) {
  setTimeout(function () { callback(value); }, 200);
}
function slow2(value, value2, callback) {
  setTimeout(function () { callback(value, value2); }, 300);
}
```

Jeder Schritt übergibt `this` als Callback an seine asynchrone Operation. Sobald die fertig ist, wird der nächste Schritt aktiv, und das Zwischenergebnis liegt direkt als Argument vor. Bemerkenswert ist, dass ein Schritt auch mehrere Werte weiterreichen kann – `slow2` gibt zwei zurück, und der folgende Schritt nimmt zwei entgegen. Das ist exakt das Waterfall-Muster, nur ohne Array, ohne Optionen, ohne Abhängigkeiten. Die `package.json` listet unter `dependencies` schlicht nichts.

## Kette gegen Pipeline

Der Unterschied zwischen den beiden Schreibweisen lässt sich gut nebeneinanderstellen. Die Callback-Kette schachtelt Zustand und Fehlerbehandlung mit jeder Ebene tiefer ineinander. Die Pipeline legt die Schritte nebeneinander und macht die Übergänge zu einem eigenen, benannten Mechanismus.

```mermaid
flowchart TB
  subgraph Kette["Callback-Kette (verschachtelt)"]
    direction TB
    a1["Schritt 1"] --> a2["  Schritt 2"]
    a2 --> a3["    Schritt 3"]
    a3 --> a4["      Ergebnis"]
  end
  subgraph Pipeline["Pipeline (modelliert)"]
    direction LR
    b1["Schritt 1"] --> b2["Schritt 2"]
    b2 --> b3["Schritt 3"]
    b3 --> b4["Ergebnis"]
  end
```

Optisch ist die Pipeline ruhiger. Wichtiger ist aber, was sich strukturell ändert. In der Pipeline existiert der Übergang von Schritt zu Schritt nur an einer einzigen Stelle, nämlich in der Bibliothek. Wenn man dort etwas verbessern will – Logging, ein Timeout, eine zentrale Fehlerweiche – dann hat man genau einen Ort dafür. In der verschachtelten Kette müsste man dieselbe Änderung in jeder Ebene wiederholen.

## Wo `cflow` an seine Grenze stößt

Und hier liegt die ehrlichste Lektion dieses kleinen Stücks Code. `cflow` modelliert die Reihenfolge sauber, aber es modelliert keinen Fehler. Es gibt keinen error-first-Kanal, keinen Abbruch, kein `try`. Wirft ein Schritt eine Ausnahme oder ruft `this` mit einem Fehlerobjekt auf, dann behandelt `Flow` das nicht – der Fehler wird einfach als nächstes Argument weitergereicht oder fällt durch. Genau das, was die Callback-Konvention mit ihrem `err`-Argument zumindest vorsieht, fehlt hier ganz.

Das ist Absicht, die Konsequenz aus „so einfach wie möglich“. Für eine Demonstration des Kontrollflusses reicht es. Für einen Service, der unter Last läuft, reicht es nicht. Damit benennt die Bibliothek selbst die Grenze, an der man weiterdenken muss: Ein Kontrollfluss ist erst dann vollständig, wenn auch der Fehlerpfad Teil des Modells ist. Wer eine eigene Flow-Funktion baut, sollte den nächsten Schritt deshalb so anlegen, dass ein Fehler die Kette abbricht und an einen einzigen Endpunkt geleitet wird, statt blind weiterzulaufen. `async.waterfall` macht genau das vor, indem es bei einem Fehler im ersten Argument sofort zum Abschluss-Callback springt.

## Rückblick aus späterer Zeit

**Redaktionelle Einordnung (Juli 2026):** Der folgende Rückblick wurde nachträglich ergänzt. Der Hauptteil des Artikels beschreibt den Stand von 2014; die Hinweise auf ES2015 und ES2017 gehören nicht zur ursprünglichen zeitlichen Perspektive.

Wer diesen Text Jahre später liest, kennt die Auflösung. Mit ES2015 kamen Generatoren, die zusammen mit Bibliotheken wie `co` synchron aussehenden asynchronen Code erlaubten. Und mit ES2017 wurde `async/await` Teil der Sprache. Damit verschwindet die Frage, die diesen Artikel trägt, fast vollständig aus dem Alltag: Die Reihenfolge schreibt man wieder von oben nach unten, der Fehlerpfad ist ein normales `try/catch`, und das Warten ist ein Schlüsselwort.

Das macht die alte Übung aber nicht wertlos. Im Gegenteil. Ein `await` sieht harmlos und synchron aus, doch dahinter steht weiterhin ein nicht-blockierender Aufruf, der Ressourcen halten kann. Die Dinge, die man sich 2014 von Hand klarmachen musste – wo die Reihenfolge erzwungen wird, wo ein Fehler die Kette abbricht, wo ein Timeout fehlt, was bei doppelt gefeuerten Fortsetzungen passiert – sind alle noch da. Sie sind nur unter einer bequemen Syntax verborgen. Wer den expliziten Kontrollfluss einmal selbst gebaut hat, erkennt diese Grenzen auch dann noch, wenn die Sprache sie nicht mehr vor die Augen hält.

## Weiterführende Quellen

- Repository cflow: https://github.com/MikeBild/cflow
- caolan/async, Dokumentation der Control-Flow-Funktionen: https://github.com/caolan/async
- Promises/A+ Spezifikation: https://promisesaplus.com/
