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


Common Use Cases