process.stdin
Exposes the readable stream connected to standard input, file descriptor 0.
process.stdinReturn value
StreamA stream connected to fd 0. It is a net.Socket (a Duplex stream) unless fd 0 refers to a file, in which case it is a Readable stream; when fd 0 is a terminal it is a tty.ReadStream.
Errors
Reading does not throw on end of input — the stream simply emits 'end'. Calling setRawMode() when process.stdin is not a TTY throws, because the method does not exist on non-TTY streams.
process.stdin is the readable side of your process: whatever a user types, or whatever the previous command in a pipeline produced. Its concrete class depends on what fd 0 points at — tty.ReadStream for a terminal, net.Socket for a pipe, fs.ReadStream for a redirected file — which is why process.stdin.isTTY is the check that matters, not instanceof.
How to read it
The modern form is for await (const chunk of process.stdin), which handles flow control for you. For line-oriented input use node:readline. The old process.stdin.resume() plus 'data' pattern still works but switches the stream into legacy flowing mode.
What trips people up
- Referencing it keeps the process alive. Once you start reading, the loop has a handle and your script will not exit. Call
process.stdin.unref()orpause()when you are done. - Nothing arrives if nothing is piped. A script run as
node tool.jswith no redirection waits at the terminal forever, which looks like a hang. Guard withif (process.stdin.isTTY)and print usage instead. - Chunk boundaries are arbitrary — a JSON document can arrive split across several chunks, so buffer before parsing.
- Ctrl-C is only intercepted after
setRawMode(true), and you must restore it afterwards or leave the user's terminal broken.
Example
// Read everything piped into the process, then report on it.
// Run as: printf 'beta\nalpha\ngamma\n' | node example.js
if (process.stdin.isTTY) {
console.error('usage: <command> | node example.js');
process.exitCode = 1;
} else {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const text = Buffer.concat(chunks).toString('utf8');
const lines = text.split('\n').filter(Boolean);
console.log('lines read:', lines.length);
console.log('sorted:', lines.sort().join(', '));
}
// $ printf 'beta\nalpha\ngamma\n' | node example.js
// lines read: 3
// sorted: alpha, beta, gammaWhen to use it
- Build a filter that transforms piped text, in the style of grep or jq.
- Prompt for a value interactively when a CLI flag was not supplied.
- Read a JSON payload piped from another command instead of taking a file path.
Frequently asked questions
How do I read piped input in a Node.js script?
Iterate the stream with `for await (const chunk of process.stdin)` and concatenate the chunks, or use node:readline for line-by-line input. Check process.stdin.isTTY first so the script prints usage instead of hanging when nothing is piped in.
Why does my Node.js script hang when reading process.stdin?
Because nothing is connected to standard input, so the stream stays open waiting for data that never arrives. Guard the read with `if (process.stdin.isTTY)` and take a file argument or print usage instead.
Why won't my script exit after reading stdin?
An active stdin stream is a handle that keeps the event loop alive. Call process.stdin.pause() or process.stdin.unref() once you have the data, and close any readline interface you created.
More examples
A line-oriented filter with readline
readline handles the hard part: input arrives in arbitrary chunks that rarely align with line boundaries, and it reassembles them. crlfDelay: Infinity stops a \r\n pair that straddles two chunks from producing a phantom blank line. Results go to stdout so the output can be piped onward, while the count goes to stderr so it does not contaminate that pipe.
import readline from 'node:readline';
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity, // treat \r\n as a single line break
});
let lineNo = 0;
for await (const line of rl) {
lineNo++;
if (line.includes('ERROR')) {
process.stdout.write(`${String(lineNo).padStart(4)}: ${line}\n`);
}
}
process.stderr.write(`scanned ${lineNo} lines\n`);
// $ cat app.log | node grep-errors.js
// 17: 2024-03-02 ERROR connection reset
// 204: 2024-03-02 ERROR retry limit exceeded
// scanned 812 linesAn interactive prompt that falls back to a flag
The isTTY check is what makes this safe in CI: without it, an unattended run would block on the prompt until the job timed out. rl.close() in the finally block is equally important — an open readline interface holds a reference to stdin, and the process will not exit while that handle is alive, even after your last line of code has run.
import readline from 'node:readline/promises';
async function getBranch() {
const flag = process.argv.indexOf('--branch');
if (flag !== -1) return process.argv[flag + 1];
if (!process.stdin.isTTY) {
throw new Error('--branch is required when input is not a terminal');
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const answer = await rl.question('branch to deploy [main]: ');
return answer.trim() || 'main';
} finally {
rl.close(); // releases stdin so the process can exit
}
}
console.log('deploying', await getBranch());
// $ node deploy.js
// branch to deploy [main]: release-2
// deploying release-2
//
// $ node deploy.js --branch main < /dev/null
// deploying mainRaw mode for single keypresses
Raw mode delivers each keystroke immediately rather than waiting for Enter, which is how menus and TUI apps work. It also disables the terminal's own Ctrl-C handling, so you must recognise yourself or the user cannot quit. The 'exit' listener is the safety net: if the program crashes with raw mode on, the shell is left without echo and the user has to type `reset` blind to recover it.
if (!process.stdin.isTTY) {
throw new Error('raw mode requires a terminal');
}
process.stdin.setRawMode(true); // no line buffering, no echo
process.stdin.resume();
process.stdin.setEncoding('utf8');
function restore() {
process.stdin.setRawMode(false);
process.stdin.pause();
}
process.stdin.on('data', (key) => {
if (key === '') { // Ctrl-C is now a normal keystroke
restore();
process.stdout.write('\ninterrupted\n');
process.exitCode = 130;
return;
}
if (key === 'q') {
restore();
process.stdout.write('\nbye\n');
return;
}
process.stdout.write(`key: ${JSON.stringify(key)}\n`);
});
// Always restore the terminal, however the program ends.
process.on('exit', () => {
if (process.stdin.isTTY) process.stdin.setRawMode(false);
});
Discussion