Signatur
Beschreibung
ReadableStreamDefaultReader ist der Standardleser der Streams-API. Ihr bekommt ihn meist über stream.getReader() oder erzeugt ihn direkt mit new ReadableStreamDefaultReader(stream). Solange ein Leser aktiv ist, ist der Stream gesperrt (stream.locked === true). Nur dieser Leser kann dann Daten entnehmen.
Die zentrale Methode ist read(). Sie gibt ein Promise zurück, das mit einem Objekt { value, done } erfüllt wird. value ist der nächste Chunk, bei Fetch-Antworten ein Uint8Array. done ist true, sobald der Stream beendet ist. Typische Einsatzfälle sind: große Downloads Stück für Stück verarbeiten, Fortschrittsanzeigen, Server-gesendete Datenströme wie NDJSON- oder LLM-Token-Streams lesen oder Daten parsen, bevor die ganze Antwort da ist.
Weitere Mitglieder:
closed: ein Promise, das erfüllt wird, wenn der Stream geschlossen wird. Bei einem Fehler oder beim Freigeben der Sperre wird es abgelehnt.cancel(reason): bricht den Stream ab. Die Quelle wird informiert, und ungelesene Daten werden verworfen.releaseLock(): gibt die Sperre frei, damit ein anderer Leser den Stream nutzen kann.
Für Byte-Streams mit eigenen Puffern („Bring Your Own Buffer“) gibt es zusätzlich ReadableStreamBYOBReader. Für die meisten Fälle reicht der Standardleser.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $stream Pflicht | ReadableStream | Nur für den Konstruktor new ReadableStreamDefaultReader(stream): der Stream, der gelesen werden soll. Ist er schon gesperrt, wird ein TypeError geworfen. |
|
| $reason | any | undefined | Für cancel(reason): ein Abbruchgrund, der an die zugrunde liegende Quelle weitergegeben wird. |
Rückgabewert
getReader() geben eine Leser-Instanz zurück. read() gibt Promise<{ value: any, done: boolean }> zurück, cancel() gibt Promise<void> zurück, closed ist ein Promise<void>.Beispiele
Fetch-Antwort Stück für Stück lesen, mit Fortschritt
const downloadWithProgress = async (url) => {
const response = await fetch(url);
const total = Number(response.headers.get('Content-Length')) || 0;
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
if (total) console.log(`Fortschritt: ${Math.round((received / total) * 100)} %`);
}
return new Blob(chunks);
};
downloadWithProgress('/datei.zip').then((blob) => console.log('Fertig:', blob.size, 'Bytes'));
Leser per Konstruktor erzeugen und eigenen Stream lesen
const stream = new ReadableStream({
start(controller) {
['a', 'b', 'c'].forEach((c) => controller.enqueue(c));
controller.close();
}
});
const reader = new ReadableStreamDefaultReader(stream);
console.log(stream.locked);
reader.closed.then(() => console.log('Stream geschlossen'));
const readAll = async () => {
let result;
while (!(result = await reader.read()).done) {
console.log('Chunk:', result.value);
}
};
readAll();
Text-Stream dekodieren und zeilenweise verarbeiten (NDJSON)
const readNdjson = async (url, onItem) => {
const response = await fetch(url);
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = '';
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split('\n');
buffer = lines.pop(); // unvollständige Zeile aufheben
lines.filter(Boolean).forEach((line) => onItem(JSON.parse(line)));
}
if (buffer.trim()) onItem(JSON.parse(buffer));
} finally {
reader.releaseLock();
}
};
readNdjson('/api/events', (item) => console.log(item));
Lesen nach Zeitlimit abbrechen
const readWithTimeout = async (url, ms) => {
const response = await fetch(url);
const reader = response.body.getReader();
const timer = setTimeout(() => reader.cancel('Zeitlimit erreicht'), ms);
let bytes = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
bytes += value.byteLength;
}
clearTimeout(timer);
return bytes;
};
readWithTimeout('/stream', 2000).then((n) => console.log('Gelesen:', n, 'Bytes'));
// Wichtig · Fallstricke
- Sperre: Solange ein Leser existiert, schlagen
getReader(),pipeTo()undresponse.text()fehl, weil der Stream gesperrt ist. Gebt die Sperre mitreleaseLock()frei, am besten in einemfinally-Block. Offeneread()-Promises werden dabei abgelehnt; ältere Implementierungen werfen stattdessen einen Fehler. - Einmal lesbar: Ein Fetch-Body lässt sich nur einmal verbrauchen. Wenn ihr ihn zweimal braucht, nutzt vorher
response.clone()oderstream.tee(). - Chunk-Grenzen: Die Chunk-Größe ist nicht festgelegt. Ein UTF-8-Zeichen oder eine JSON-Zeile kann auf zwei Chunks verteilt sein. Dekodiert daher mit
TextDecoderStreamoderTextDecodermit{ stream: true }und puffert unvollständige Zeilen. - Fehler: Tritt im Stream ein Fehler auf, wird
read()abgelehnt. Fangt das mittry/catchab. - Alternative: In modernen Umgebungen (Node.js, Firefox, Chrome 124+) ist
ReadableStreamasynchron iterierbar (for await (const chunk of stream)). Das holt und löst den Leser automatisch, ist aber in Safari erst seit Kurzem verfügbar. - Kompatibilität: In allen modernen Browsern verfügbar. Der Konstruktor
new ReadableStreamDefaultReader()wird etwas später unterstützt alsgetReader(). Nehmt im ZweifelgetReader(). Der Internet Explorer wird nicht unterstützt.