writable.write()
Write one chunk to a stream and learn whether its buffer is now full.
writable.write(chunk[, encoding][, callback])- Added in Node.js v0.9.4
Parameters
| Parameter | Type | Description |
|---|---|---|
$chunk | string | Buffer | TypedArray | DataView | any | Optional data to write. For streams not operating in object mode, chunk must be a string, Buffer, TypedArray or DataView. For object mode streams, chunk may be any JavaScript value other than null. |
$encodingoptional | string | null= 'utf8' | The encoding, if chunk is a string. |
$callbackoptional | Function | Callback for when this chunk of data is flushed. Called with the error as its first argument if the write fails, asynchronously and before 'error' is emitted. |
Return value
booleanfalse if the stream wishes for the calling code to wait for the 'drain' event to be emitted before continuing to write additional data; otherwise true.
Errors
Throws ERR_STREAM_NULL_VALUES when chunk is null, ERR_INVALID_ARG_TYPE for a non-Buffer chunk outside object mode, and emits ERR_STREAM_WRITE_AFTER_END if called after end().
Changelog
- PHP v22.0.0The chunk argument can now be a TypedArray or DataView instance (also in v20.13.0).
- PHP v8.0.0The chunk argument can now be a Uint8Array instance.
- PHP v6.0.0Passing null as the chunk parameter is always invalid now, even in object mode.
write() hands one chunk to a writable stream. The value it returns is the whole point: true means the internal buffer is still below highWaterMark and you may keep going; false means stop and wait for the 'drain' event.
When to reach for it
When you are the producer — generating records, serialising a report, responding to an HTTP request in a loop — and the data is not already a stream. If it is a stream, use pipeline(), which handles all of this for you.
What trips people up
Ignoring the return value. false is not an error and nothing crashes, so a loop that writes a million rows without checking looks fine in development and works. In production it buffers everything in memory: Node keeps accepting chunks until RSS climbs, garbage collection degrades, and the process aborts. Node's own documentation notes that writing to a socket that never drains is a remotely exploitable denial of service, because a peer that stops reading makes your process grow without bound.
The callback is also misread as "written to disk". It fires when the stream has handled the chunk, which for a file means handed to the OS, and it is where a per-chunk error surfaces.
In a for await loop, prefer an async generator inside pipeline() over manual drain handling.
Example
When to use it
- Stream generated rows into a CSV file without buffering the whole report
- Respond to an HTTP request incrementally as records are produced
- Feed a compressor chunk by chunk from a loop that computes the data
- Detect and honour backpressure when the consumer is slower than the producer
Frequently asked questions
What does it mean when writable.write() returns false in Node.js?
It means the stream's internal buffer has reached highWaterMark and you should stop writing until the 'drain' event fires. It is a flow-control signal, not an error, and the chunk you just wrote was still accepted.
How do I handle backpressure when writing to a stream?
Check the return value of write(), and when it is false await the 'drain' event — for example with await once(dest, 'drain') — before writing again. Using pipeline() with a generator source avoids the loop entirely.
Does the write() callback mean the data hit the disk?
No, it fires once the stream has finished handling the chunk, which for a file stream means it was passed to the operating system. Use fs.fsync() if you need a durability guarantee.
More examples
The correct manual backpressure loop
events.once() turns the 'drain' event into an awaitable, which is the cleanest way to write this loop. Memory stays flat regardless of how many rows there are, because the loop pauses whenever the file stream's buffer fills. Drop the if-statement and the same code queues all 100,000 lines in memory at once.
import fs from 'node:fs';
import { once } from 'node:events';
async function writeRows(dest, rows) {
for (const row of rows) {
// If write() returns false, wait for 'drain' before continuing.
if (!dest.write(JSON.stringify(row) + '\n')) {
await once(dest, 'drain');
}
}
dest.end();
await once(dest, 'finish');
}
const out = fs.createWriteStream('report.ndjson');
const rows = Array.from({ length: 100_000 }, (_, i) => ({ i }));
await writeRows(out, rows);
console.log('rows written:', rows.length);Letting pipeline handle backpressure for you
Functionally identical to the manual loop above, and immune to the mistake it guards against. Whenever your producer can be expressed as a generator, this form removes the possibility of ignoring backpressure — which is why explicit write() loops are worth avoiding when there is an alternative.
import { pipeline } from 'node:stream/promises';
import { Readable } from 'node:stream';
import fs from 'node:fs';
function* rows() {
for (let i = 0; i < 100_000; i++) {
yield JSON.stringify({ i }) + '\n';
}
}
// No write(), no 'drain', no return value to check:
// the generator is pulled only as fast as the disk accepts data.
await pipeline(Readable.from(rows()), fs.createWriteStream('report.ndjson'));
console.log('done');
Discussion