How to Use ReadableStream with JavaScript Fetch
This article provides an overview of the ReadableStream
interface in JavaScript and explains how to consume streaming responses
using the Fetch API. You will learn the fundamental concepts behind web
streams, how the response.body property exposes incoming
network data, and how to read, decode, and process data incrementally
using modern asynchronous JavaScript patterns.
What is the ReadableStream Interface?
The ReadableStream interface is a core component of the
Web Streams API. It represents a source of byte data that can be read
sequentially as it becomes available over time, rather than waiting for
the entire payload to be downloaded into memory.
By breaking data into smaller pieces called chunks,
ReadableStream allows applications to process data on the
fly. This significantly improves memory efficiency and perceived
performance, particularly for large payloads, media files, or real-time
data streams such as AI-generated text.
How Fetch Implements ReadableStream
When you make a network request using fetch(), the
returned Response object provides the payload via the
response.body property.
Instead of consuming the entire body at once using methods like
response.json() or response.text(),
response.body is exposed directly as a
ReadableStream<Uint8Array>. This allows you to
intercept and handle raw network packets as they arrive from the
server.
Consuming a Streaming Fetch Body
There are two primary ways to consume a ReadableStream
in JavaScript: using a stream reader or using asynchronous
iteration.
1. Using
getReader() and a while Loop
The traditional and most widely supported approach is to obtain a
reader via response.body.getReader() and process chunks in
a loop using reader.read().
async function fetchAndStream(url) {
const response = await fetch(url);
if (!response.body) {
throw new Error('ReadableStream is not supported or body is empty.');
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let isDone = false;
while (!isDone) {
const { value, done } = await reader.read();
if (done) {
isDone = true;
break;
}
// Decode the Uint8Array chunk into text
const textChunk = decoder.decode(value, { stream: true });
console.log('Received chunk:', textChunk);
}
console.log('Stream completed.');
}2. Using
for await...of (Async Iteration)
Modern browsers and Node.js runtimes support async iteration directly
on ReadableStream instances, providing a cleaner
syntax:
async function streamWithAsyncIterator(url) {
const response = await fetch(url);
const decoder = new TextDecoder('utf-8');
for await (const chunk of response.body) {
const textChunk = decoder.decode(chunk, { stream: true });
console.log('Received chunk:', textChunk);
}
console.log('Stream completed.');
}Key Components to Remember
Uint8Array: Stream chunks arrive as raw binary buffers. UseTextDecoderto convert them into readable strings.- Stream Locking: Calling
response.body.getReader()locks the stream to that reader. No other reader can read the stream until it is released usingreader.releaseLock(). - Cancellation: You can abort an active stream at any
time using
reader.cancel()or by passing anAbortSignalto the initialfetch()call.
Common Use Cases
- Large File Downloads: Displaying accurate, incremental progress bars without loading the full file into RAM.
- Generative AI Responses: Rendering text tokens to the UI character-by-character as they are generated by a language model API.
- Real-Time Data Feeds: Processing continuous log streams or Server-Sent Events (SSE) efficiently.