Skip to main content

Streams: ReadableStream and WritableStream

A stream is a sequence of data that arrives gradually rather than all at once. A plain fetch().text() waits until everything has arrived and only then returns a result, while a stream (response.body) lets you process the data piece by piece, as it comes in, chunk by chunk.

Theory

TL;DR

  • A stream is data arriving in chunks rather than in one block.
  • ReadableStream is a source, WritableStream is a sink, TransformStream is a converter.
  • response.body after fetch() is a ready-made ReadableStream.
  • reader.read() returns a promise with { done, value }, where value is a Uint8Array.
  • Streams are chained with pipeThrough() and pipeTo().
  • The main benefit: processing large volumes without loading everything into memory.

Quick example

javascript
const response = await fetch('https://example.com/large-file.txt'); // response.body is a ReadableStream const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; console.log('chunk size:', value.length); }

What happens:

  1. fetch returns a Response whose .body is a ReadableStream.
  2. The .getReader() method creates a reader that hands data back in chunks.
  3. reader.read() returns a promise with { done, value }:
    • value is a piece of data (type Uint8Array);
    • done: true means the stream is finished.

This is how you download gigabytes without holding the whole file in memory.

Stream types in JavaScript

TypePurpose
ReadableStreamdata source (reading)
WritableStreamdata sink (writing)
TransformStreamintermediate processing (read plus write)
DuplexStreamtwo-way, a socket for example

Writing data to a WritableStream

javascript
const stream = new WritableStream({ write(chunk) { console.log('writing chunk:', chunk); }, close() { console.log('stream finished'); }, }); const writer = stream.getWriter(); await writer.write('Hello'); await writer.write('World'); await writer.close();

Explanation:

  • WritableStream accepts data in pieces through write(chunk);
  • close() is called when the stream is finished;
  • it is handy for sending data, writing files, logging and so on.

TransformStream and converting on the fly

javascript
const upperCaseStream = new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk.toUpperCase()); }, }); const writer = upperCaseStream.writable.getWriter(); const reader = upperCaseStream.readable.getReader(); await writer.write('hello '); await writer.write('world!'); await writer.close(); while (true) { const { done, value } = await reader.read(); if (done) break; console.log(value); // "HELLO " "WORLD!" }

A TransformStream joins an input stream (writable) and an output stream (readable) and lets you convert data on the fly.

Piping streams with pipeTo and pipeThrough

Streams can be connected to each other like water pipes:

javascript
await readableStream .pipeThrough(transformStream) .pipeTo(writableStream);

In other words:

  • pipeThrough applies a transformation and returns a new readable;
  • pipeTo sends the result into a final sink.

A practical example with fetch() and TextDecoderStream:

javascript
const response = await fetch('https://example.com/data.txt'); const textStream = response.body .pipeThrough(new TextDecoderStream()) // decode bytes into text .pipeThrough( new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk.toUpperCase()); // transform the text }, }) ); const reader = textStream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; console.log(value); }

Every piece of the file is first turned into text, then uppercased, and processed immediately, without waiting for the download to finish.

Streams in Node.js and why they matter

Node.js has its own implementation (fs.createReadStream, stream.Writable and others), but the concept is the same:

javascript
import fs from 'fs'; const readStream = fs.createReadStream('file.txt', { encoding: 'utf-8' }); readStream.on('data', chunk => console.log('Chunk:', chunk)); readStream.on('end', () => console.log('Done'));

Node.js streams are built on EventEmitter, while browser streams are built on promises and controllers. Modern Node.js also ships Web Streams through node:stream/web, so the code can move between environments.

AdvantageDescription
PerformanceNo need to wait for a whole file to download
Low memory useProcessing happens on the fly, in chunks
CompositionStreams can be joined into chains
Two-way communicationUsed by WebSocket, fetch(), Response, Body
ControlReading can be paused and resumed (backpressure)

A simple analogy: streams are a conveyor belt. The data does not arrive whole, it travels along the belt in chunks, and at every stage (Readable, Transform, Writable) you can process, filter and store it.

TypePurposeExample use
ReadableStreamData sourcefetch().body, file.stream()
WritableStreamData sinkWriting to a file, a log, the network
TransformStreamConverterCompression, encryption, filtering
pipeTo / pipeThroughConnecting streamsreadable.pipeThrough(transform).pipeTo(writable)

Common mistakes

  • Reading response.body after response.text(). A response body can only be consumed once: after text(), json() or blob() the stream is already locked. If you need both, use response.clone().
  • Forgetting to release the reader. While getReader() is held, the stream stays locked. Call reader.releaseLock() or read the stream to the end.
  • Ignoring backpressure. Writing into a WritableStream without await writer.write(...) lets the queue grow, and the memory advantage of streaming disappears.
  • Assuming value is a string. response.body yields Uint8Array; to get text you need TextDecoderStream or TextDecoder.
  • Not handling errors in the chain. pipeTo() returns a promise that can reject; without a catch, a network failure goes unnoticed.
  • Confusing Node.js streams with Web Streams. They have different APIs: the first works through data and end events, the second through promises and controllers.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.