Streams API — ReadableStream & WritableStream
debt(d7/e5/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). Detection hints are not specified for this term. Stream errors (missing try/catch around read loops), reader lock leaks (forgetting cancel()), and body consumption conflicts (mixing .json()/.text() with streaming) are all runtime failures or silent logic bugs that require deliberate testing or code review to catch. No built-in linter or default tooling flags these patterns.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes a single read loop pattern, but the common mistakes reveal that proper remediation often requires: wrapping the read loop in try/catch error handling, adding reader.cancel() cleanup logic, and potentially introducing buffering logic for line-by-line splitting. This goes beyond a one-line swap and typically involves restructuring how the response is consumed across a component.
Closest to 'localised tax (one component pays)' (b3). The Streams API choice is isolated to the code path that consumes HTTP responses (primarily LLM streaming). It does not define the system's shape; most of the codebase remains unaffected. However, within that component, careful reader lock management and error handling must be maintained.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception directly states the trap: developers expect the Streams API to split on logical boundaries (lines, JSON objects) when it actually splits on network byte chunks. TextDecoderStream does not split on newlines despite its name suggesting text semantics. Additionally, mixing streaming consumption with traditional response.json()/response.text() methods contradicts the pattern in simpler fetch code. This is a documented gotcha that contradicts intuition from other streaming or response-parsing APIs.
Also Known As
TL;DR
Explanation
The Fetch API integrates with Streams: response.body is a ReadableStream. You can pipe it through a TransformStream (e.g. a TextDecoderStream) into a WritableStream or consume it manually with a reader. ReadableStream.pipeThrough() chains transforms; pipeTo() connects to a writable. Backpressure propagates upstream automatically — if the consumer is slow, the producer is throttled. Streams are the correct tool for streaming LLM responses (server-sent tokens), processing large CSV uploads without loading them in memory, and implementing video/audio pipelines. Node.js has its own streams API (streams2); the browser Web Streams API is now available in Node 18+ via the global scope.
Common Misconception
Why It Matters
Common Mistakes
- Not handling stream errors — wrap the read loop in try/catch; network errors throw inside the loop.
- Forgetting to release the reader lock — if you break early from the loop, call reader.cancel() to release the lock so other consumers can use the stream.
- Mixing response.json() or response.text() with streaming — calling these after partially reading the body causes an error; choose one approach.
- Assuming TextDecoderStream splits on newlines — it splits on byte chunks from the network, not lines; buffer and split manually if you need line-by-line events.
Code Examples
// ❌ Waiting for full response before displaying anything
const response = await fetch('/api/llm-chat', {
method: 'POST',
body: JSON.stringify({ message })
});
const text = await response.text(); // Waits for ALL tokens — no streaming
displayEl.textContent = text; // User sees nothing for 10+ seconds
// ✅ Stream LLM tokens as they arrive
const response = await fetch('/api/llm-chat', {
method: 'POST',
body: JSON.stringify({ message })
});
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
fullText += value;
displayEl.textContent = fullText; // Update UI on each chunk
}
// ✅ Transform stream pipeline (e.g. decompress + decode)
const decompressed = response.body
.pipeThrough(new DecompressionStream('gzip'))
.pipeThrough(new TextDecoderStream());
for await (const chunk of decompressed) {
process(chunk);
}