Zurück zum Blog
Apr. 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Async-Iteratoren und -Generatoren in JavaScript

#javascript#async

for...of-Schleifen laufen über Arrays und andere iterierbare Objekte jeweils einen Wert nach dem anderen. for await...of tut dasselbe, wenn jeder Wert Zeit zur Erzeugung braucht – paginierte API-Ergebnisse lesen, eine große Datei streamen oder Events verarbeiten, sobald sie eintreffen –, ohne dass man Promises und Schleifenzustand von Hand jonglieren muss. Der Mechanismus hinter beiden sind Generatoren, und sobald man einen schreiben kann, wird eine ganze Kategorie von „verarbeite diesen Strom von Dingen"-Code deutlich einfacher.

Normale Generatoren: pausierbare Funktionen

Eine Generatorfunktion (function*) kann sich mit yield selbst anhalten und später fortsetzen, wobei ihr lokaler Zustand zwischen den Fortsetzungen erhalten bleibt.

function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}

for (const value of countTo(3)) {
  console.log(value); // 1, 2, 3
}

Der Aufruf countTo(3) führt den Funktionskörper nicht aus – er gibt einen Iterator zurück. Jeder .next()-Aufruf (den for...of im Hintergrund macht) führt die Funktion bis zum nächsten yield aus, gibt diesen Wert zurück und pausiert erneut. Das ist der gesamte Mechanismus: eine Funktion, die fortgesetzt werden kann, statt immer nur von Anfang bis Ende zu laufen.

Async-Generatoren: dieselbe Idee, mit Warten

Ein Async-Generator (async function*) kann vor dem yield ein await ausführen, sodass jeder Wert von einer asynchronen Operation abhängen kann – einer Netzwerkanfrage, einer Datenbankabfrage, einem Timer.

async function* fetchAllUsers() {
  let page = 1;
  while (true) {
    const response = await fetch(`/api/users?page=${page}`);
    const { users, hasMore } = await response.json();

    yield* users; // yield each user individually, not the whole page

    if (!hasMore) return;
    page++;
  }
}
for await (const user of fetchAllUsers()) {
  console.log(user.name);
}

Der Konsument muss nie wissen, dass Pagination stattfindet. Er sieht einen einzigen Strom von user-Objekten und verarbeitet sie einzeln; der Generator versteckt die Paging-Schleife, die fetch-Aufrufe und die „Bin ich schon fertig"-Logik vollständig.

Warum nicht einfach ein Array von Promises zurückgeben?

Die Alternative – alle Seiten vorab holen, per Promise.all zusammenführen und das Ergebnis abflachen – funktioniert, bedeutet aber, den gesamten Datensatz im Speicher zu halten und auf jede Seite zu warten, bevor der erste Datensatz verarbeitet wird. Ein Async-Generator liefert, sobald jeder Wert bereit ist:

// Eager: waits for everything, holds it all in memory
async function fetchAllUsersEager() {
  const pages = await Promise.all(
    pageNumbers.map((p) => fetch(`/api/users?page=${p}`).then((r) => r.json())),
  );
  return pages.flatMap((p) => p.users);
}

// Lazy: starts producing values immediately, one page at a time
async function* fetchAllUsersLazy() {
  // ...as above
}

Bei einem Datensatz mit 10.000 Nutzern auf 100 Seiten kann die eager-Variante erst mit Nutzer Nr. 1 beginnen, wenn alle 100 Anfragen abgeschlossen sind. Die Generator-Variante beginnt mit Nutzer Nr. 1, sobald Seite 1 eintrifft – und ebenso wichtig: Ein Konsument, der nur die ersten 20 Nutzer braucht, kann früh aufhören, ohne dass der Generator je die Seiten 2 bis 100 holt.

Frühes Abbrechen stoppt die Arbeit tatsächlich

Ein break aus einer for await...of-Schleife ruft das .return() des Generators auf, mit dem die Laufzeitumgebung anstehende finally-Blöcke ausführt – so gibt man Ressourcen sauber frei, nicht nur so beendet man die Iteration.

async function* readLines(filePath) {
  const fileHandle = await openFile(filePath);
  try {
    let line;
    while ((line = await fileHandle.readLine()) !== null) {
      yield line;
    }
  } finally {
    console.log('closing file handle');
    await fileHandle.close();
  }
}

for await (const line of readLines('access.log')) {
  if (line.includes('ERROR')) {
    console.log('found it:', line);
    break; // triggers the generator's finally block — file still gets closed
  }
}

Ohne das finally des Generators bliebe das Dateihandle bei frühem Abbruch offen. Das ist dieselbe Garantie, die ein normales try/finally um eine Schleife gibt, nur erweitert auf eine Ressource, deren Lebensdauer mehrere await-Punkte umspannt.

Ein eigenes Async-Iterable bauen

Alles, was Symbol.asyncIterator implementiert, funktioniert mit for await...of – man braucht dafür nicht unbedingt eine Generatorfunktion, auch wenn sie meist der einfachste Weg ist. Das ist nützlich, um Dinge zu kapseln, die Werte über die Zeit erzeugen, aber selbst keine Generatoren sind, etwa einen WebSocket-Nachrichtenstrom:

function messagesFrom(socket) {
  return {
    [Symbol.asyncIterator]() {
      const queue = [];
      let resolveNext;

      socket.addEventListener('message', (event) => {
        if (resolveNext) {
          resolveNext({ value: event.data, done: false });
          resolveNext = null;
        } else {
          queue.push(event.data);
        }
      });

      return {
        next() {
          if (queue.length > 0) {
            return Promise.resolve({ value: queue.shift(), done: false });
          }
          return new Promise((resolve) => { resolveNext = resolve; });
        },
      };
    },
  };
}
for await (const message of messagesFrom(socket)) {
  console.log('received:', message);
}

Damit wird eine API im Event-Emitter-Stil zu etwas, über das man direkt iterieren kann, statt einen Callback zu registrieren und Zustand außerhalb der Schleife von Hand zu verwalten.

Wann man dazu greifen sollte

Async-Generatoren sind das richtige Werkzeug, wann immer du eine Folge von Werten verarbeitest, die über die Zeit eintreffen, und der Konsument nicht wissen soll, wie sie eintreffen – Pagination, gestreamte Antworten, in Chunks gelesene Dateien, WebSocket-/SSE-Nachrichtenströme. Hast du den vollständigen Datensatz bereits in der Hand (etwa das Ergebnis eines einzelnen fetch-Aufrufs), sind ein einfaches Array und for...of simpler, und es gibt keinen Grund, einen Generator einzuführen. Greife gezielt zu async function*, wenn „den nächsten Wert erzeugen" selbst Warten erfordert.

NeuerEine sanfte Einführung in WebSocketsÄlterMemoization in JavaScript – jenseits von useMemo