process.stdin

Exposes the readable stream connected to standard input, file descriptor 0.

Signature
process.stdin

Return 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() or pause() when you are done.
  • Nothing arrives if nothing is piped. A script run as node tool.js with no redirection waits at the terminal forever, which looks like a hang. Guard with if (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

Example · javascript
// 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, gamma

When 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.

Example · javascript
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 lines

An 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.

Example · javascript
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 main

Raw 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.

Example · javascript
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

  • Be the first to comment on this lesson.