Skip to main content

How do streams (ReadableStream, WritableStream) work?

What a stream is

A stream is a sequence of data that arrives gradually, not all at once.

The main difference from regular data:

  • a regular fetch().text() waits until everything arrives → then returns the result;
  • a stream (fetch().body) lets you process data in pieces, as soon as each part arrives (chunk by chunk).

Types of streams in JavaScript

TypePurpose
ReadableStreama data source (reading)
WritableStreama data sink (writing)
TransformStreamintermediate processing (reading + writing)
DuplexStreambidirectional (e.g. a socket)

Example: reading data from a stream (ReadableStream)

Imagine you're requesting a large file:

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('Received a chunk of size:', value.length); }

What happens:

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

This lets you download gigabytes without loading everything into RAM.

Example: writing data to a WritableStream

javascript
const stream = new WritableStream({ write(chunk) { console.log('Writing a 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 parts via write(chunk);
  • close() is called when the stream is finished;
  • can be used for sending data, writing files, logging, and so on.

Example: TransformStream (transforming a stream)

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!" }

TransformStream connects an input stream (writable) and an output stream (readable), and lets you transform data "on the fly".

Using streams with pipeTo / pipeThrough

JS streams can be connected (piped) like plumbing pipes

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

This way:

  • pipeThrough applies a transformation;
  • pipeTo passes the result to the next stream.

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); }

Now every piece of the file is converted into text, then to uppercase, and processed right away.

In Node.js, streams work similarly

Node.js has its own implementation (fs.createReadStream, stream.Writable, etc.), 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 based on EventEmitter, while browser streams are based on promises and controllers.

Why streams matter

BenefitDescription
PerformanceNo need to wait for the whole file to load
Memory savingsProcessing happens "on the fly", in pieces
CompositionStreams can be chained together
Two-way communicationUsed in WebSocket, fetch(), Response, Body
ControlReading can be paused/resumed

A simple analogy

Streams are like a conveyor belt

Data doesn't arrive all at once - it "rides" along the belt in portions (chunks), and at each stage (Readable → Transform → Writable) you can process, filter and store it.

Summary

TypePurposeUsage example
ReadableStreamA data sourcefetch().body, file.stream()
WritableStreamA data sinkWriting to a file, a log, the network
TransformStreamA transformerCompression, encryption, filtering
pipeTo / pipeThroughConnecting streamsreadable.pipeThrough(transform).pipeTo(writable)

Short Answer

Interview ready
Premium

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