process.stdout.write()
Writes raw data to standard output without appending a newline.
process.stdout.write(chunk[, encoding][, callback])Parameters
| Parameter | Type | Description |
|---|---|---|
$chunk | string | Buffer | TypedArray | DataView | The data to write. process.stdout is not in object mode, so the chunk must be a string or a binary view — passing an object or a number throws. |
$encodingoptional | string | null= 'utf8' | The encoding to use when chunk is a string. |
$callbackoptional | Function | Called once this chunk has been fully handled, or with an error as its first argument if the write failed. Always called asynchronously. |
Return value
booleanfalse if the internal buffer has passed its highWaterMark and the caller should wait for the 'drain' event before writing more; otherwise true.
Errors
Throws ERR_INVALID_ARG_TYPE if chunk is not a string, Buffer, TypedArray or DataView, and ERR_STREAM_NULL_VALUES if chunk is null.
Changelog
- PHP v22.0.0The chunk argument can now be a TypedArray or DataView instance (also backported to v20.13.0).
- PHP v8.0.0The chunk argument can now be a Uint8Array instance.
process.stdout is a writable stream connected to file descriptor 1, and write() is the raw way to put bytes on it. Unlike console.log() it adds no newline, applies no util.format() substitution, and returns a boolean telling you whether the stream wants you to slow down.
When to reach for it
- Progress indicators and spinners, where you redraw one line with
\rinstead of scrolling. - Emitting binary output — a generated image or archive piped to another command.
- High-volume output such as NDJSON export, where you need to honour backpressure.
What trips people up
The return value is real, not decorative. Once it returns false, continuing to write buffers everything in memory; a script that dumps a million lines into a pipe nobody is reading will grow until it aborts. Wait for 'drain'.
Whether the write is synchronous depends on what fd 1 points at: files are synchronous everywhere, TTYs are synchronous on POSIX and asynchronous on Windows, and pipes are the reverse. That asymmetry is exactly why process.exit() can truncate output over a pipe.
Only strings and binary views are accepted — write(42) throws.
Example
When to use it
- Redraw a percentage counter on one line while a long upload runs.
- Pipe generated binary output, such as a PNG buffer, straight into another command.
- Stream a large NDJSON export while respecting backpressure so memory stays flat.
Frequently asked questions
How do I print without a newline in Node.js?
Use process.stdout.write('text') instead of console.log, since write() sends exactly the characters you give it. Add '\n' yourself when you do want to end the line.
What does process.stdout.write returning false mean?
It means the stream's internal buffer has exceeded its highWaterMark and you should stop writing until the 'drain' event fires. Ignoring it does not lose data, but it buffers everything in memory and can grow until the process aborts.
Why is my process.stdout.write output missing?
Most often the process exited before the write flushed, because writes to a pipe are asynchronous on POSIX and process.exit() does not wait for them. Pass a callback to write() and exit from inside it, or set process.exitCode instead of calling exit().
More examples
A progress counter on a single line
Each write starts with a carriage return, so the terminal keeps repainting one row rather than printing five. This only works on a TTY — piped into a file the \r characters are stored literally, which is why real CLIs guard the whole block with `if (process.stdout.isTTY)` and fall back to plain lines otherwise.
const total = 5;
for (let done = 1; done <= total; done++) {
const pct = Math.round((done / total) * 100);
const bar = '#'.repeat(done).padEnd(total, '.');
// \r returns the cursor to column 0 so the next write overwrites the line.
process.stdout.write(`\r[${bar}] ${String(pct).padStart(3)}%`);
await new Promise((r) => setTimeout(r, 200));
}
process.stdout.write('\ndone\n');
// Final state of the terminal:
// [#####] 100%
// doneRespecting backpressure on a large dump
Piped into gzip, write() starts returning false once the pipe buffer fills. Awaiting 'drain' hands control back to the event loop until the consumer reads, keeping resident memory roughly constant. Without the check the loop would queue all 200,000 lines in memory first. The status line goes to stderr so it does not pollute the piped data.
const rows = Array.from({ length: 200_000 }, (_, i) => ({ id: i, ok: true }));
async function dump() {
for (const row of rows) {
const line = JSON.stringify(row) + '\n';
if (!process.stdout.write(line)) {
// The buffer is full. Stop until the consumer catches up.
await new Promise((resolve) => process.stdout.once('drain', resolve));
}
}
}
await dump();
process.stderr.write('export finished\n');
// $ node dump.js | gzip > rows.json.gz
// export finishedBinary output and the console.log difference
This is the practical boundary between the two APIs. console.log is a formatter built on top of this stream: it runs util.format, inspects objects, and adds a newline. write() is the stream itself and takes only strings or binary views, which is what makes it the correct choice when the bytes must reach the pipe untouched.
// console.log formats and appends \n; write() does neither.
console.log('%s scored %d', 'ada', 96); // "ada scored 96\n"
process.stdout.write('%s scored %d\n'); // literally "%s scored %d"
// Objects: console.log inspects, write() throws.
console.log({ a: 1 }); // { a: 1 }
try {
process.stdout.write({ a: 1 });
} catch (err) {
console.log('write threw:', err.code); // ERR_INVALID_ARG_TYPE
}
// Binary goes straight through.
const png = Buffer.from('89504e470d0a1a0a', 'hex');
process.stdout.write(png); // 8 raw bytes, no encoding applied
Discussion