Readable streams represent a source of data delivered in chunks, files, HTTP request bodies, and more. This lesson covers exactly how to consume them, including modern async iteration.
What Is a Readable Stream?
A Readable stream is an object that produces data over time. You can consume it in a few different ways: listening to 'data' events (flowing mode), manually calling .read() (paused mode), piping it to a writable destination, or, in modern Node.js, using for await...of to iterate over its chunks with clean async syntax.
Readable streams start in "paused" mode and switch to "flowing" mode as soon as you attach a 'data' listener, call .pipe(), or start async iteration, at which point chunks start arriving automatically as fast as the source can produce them (and the destination can accept them).
import fs from 'node:fs';
const stream = fs.createReadStream('./data.csv', 'utf8');
for await (const chunk of stream) {
console.log('Received chunk of length', chunk.length);
}
console.log('Stream finished.');
for await...of is the cleanest modern way to consume a Readable stream, it reads naturally and automatically handles backpressure and errors thrown inside the loop.
'data' fires once per chunk; 'end' fires once, after all data has been consumed.
'error' must always be handled, or the process will crash on stream failures.
for await...of combines chunk iteration and completion into one clean loop.
Chunks are Buffer objects by default, unless you specify an encoding (like 'utf8').
Readable Streams Cheatsheet
Core events and methods on a Readable stream instance.
Event/Method
Purpose
'data'
Fires with each chunk in flowing mode
'end'
Fires once all data has been consumed
'error'
Fires on failure, must always be handled
.pause()
Switch to paused mode, stop emitting 'data'
.resume()
Switch back to flowing mode
for await...of
Modern async iteration over chunks
Creating a Custom Readable Stream
You can build your own Readable stream when data comes from something other than a file or network socket, for example, generating rows from an in-memory array or a database cursor, one chunk at a time.
import { Readable } from 'node:stream';
const numbers = [1, 2, 3, 4, 5];
let index = 0;
const numberStream = new Readable({
objectMode: true,
read() {
if (index < numbers.length) {
this.push(numbers[index++]);
} else {
this.push(null); // signals the end of the stream
}
},
});
for await (const num of numberStream) {
console.log('Got number:', num);
}
Calling this.push(null) is how a custom Readable stream signals that there is no more data, triggering the 'end' event.
Object Mode Streams
By default, streams work with Buffer/string chunks. Setting objectMode: true lets a stream emit arbitrary JavaScript values (objects, numbers, arrays) instead, which is common when streaming structured data like database rows rather than raw bytes.
Common Mistakes
Attaching both 'data' listeners and manual .read() calls, mixing flowing and paused modes inconsistently.
Forgetting to handle the 'error' event, letting a failed stream crash the process.
Assuming chunks always arrive at a fixed size, chunk boundaries are not guaranteed to align with logical units like lines or records.
Not using objectMode when streaming non-buffer/string data, causing type errors.
Key Takeaways
Readable streams produce data over time, consumable via events, .pipe(), or async iteration.
for await...of is the cleanest modern pattern for consuming a Readable stream.
Custom Readable streams implement a read() method and call this.push(null) to signal completion.
objectMode: true allows streaming structured JavaScript values instead of raw buffers.
Pro Tip
Prefer for await...of over manual 'data'/'end' event listeners in new code, it reads more clearly, and any error thrown inside the loop propagates naturally instead of needing a separate 'error' listener.
You now understand Readable streams thoroughly. Next, learn Writable Streams, the other half of most stream pipelines.