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
| Type | Purpose |
|---|---|
| ReadableStream | a data source (reading) |
| WritableStream | a data sink (writing) |
| TransformStream | intermediate processing (reading + writing) |
| DuplexStream | bidirectional (e.g. a socket) |
Example: reading data from a stream (ReadableStream)
Imagine you're requesting a large file:
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:
fetchreturns aResponse, whose.bodyis a ReadableStream.- The
.getReader()method creates a reader that returns data in portions (chunks). reader.read()returns a promise with{ done, value }.
value- a piece of data (typeUint8Array),done: true- the stream has ended.
This lets you download gigabytes without loading everything into RAM.
Example: writing data to a WritableStream
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:
WritableStreamaccepts data in parts viawrite(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)
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
await readableStream
.pipeThrough(transformStream)
.pipeTo(writableStream);This way:
pipeThroughapplies a transformation;pipeTopasses the result to the next stream.
Example with fetch() and TextDecoderStream
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:
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
| Benefit | Description |
|---|---|
| Performance | No need to wait for the whole file to load |
| Memory savings | Processing happens "on the fly", in pieces |
| Composition | Streams can be chained together |
| Two-way communication | Used in WebSocket, fetch(), Response, Body |
| Control | Reading 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
| Type | Purpose | Usage example |
|---|---|---|
| ReadableStream | A data source | fetch().body, file.stream() |
| WritableStream | A data sink | Writing to a file, a log, the network |
| TransformStream | A transformer | Compression, encryption, filtering |
| pipeTo / pipeThrough | Connecting streams | readable.pipeThrough(transform).pipeTo(writable) |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.