How to Take Input in Node.js: 4 Methods for Console User Interaction

To take input in Node.js, use the built-in node:readline module for line-based interaction, node:readline/promises for async/await syntax, async iterators for continuous streams, or raw mode with emitKeypressEvents for character-level control.

When building command-line applications with the nodejs/node runtime, capturing user input from the console requires understanding Node.js stream architecture. The node:readline module provides the canonical solution for how to take input in node js applications, wrapping process.stdin and process.stdout in a manageable interface that supports callbacks, promises, and async iteration.

Understanding the Core Architecture

The Node.js input system operates through layered abstractions built on standard streams. At the foundation, process.stdin provides a Readable stream of raw bytes from the terminal, while process.stdout handles output as a Writable stream.

The lib/readline.js file implements the Interface class that buffers input until it detects end-of-line sequences (\n, \r, or \r\n). Upon detection, it emits a 'line' event or resolves a question promise with the buffered string. This architecture ensures cross-platform compatibility across Windows, macOS, and Linux while maintaining non-blocking I/O.

Method 1: Callback-Based Input with node:readline

The traditional approach uses the core node:readline module with callback functions. This pattern is implemented in lib/readline.js and remains the most widely supported method across all Node.js versions.

const readline = require('node:readline');
const { stdin: input, stdout: output } = require('node:process');

const rl = readline.createInterface({ input, output });

rl.question('What is your name? ', (answer) => {
  console.log(`Hello, ${answer}!`);
  rl.close();
});

This method requires explicit resource management through rl.close() to prevent the process from hanging. The callback executes when the user presses Enter, receiving the input string minus the line terminator.

Method 2: Promise-Based Input with node:readline/promises

Since Node.js v17.0.0, the node:readline/promises submodule provides a Promise-based API that enables cleaner async/await syntax. This implementation wraps the callback interface in lib/readline.js with Promise resolution logic.

import * as readline from 'node:readline/promises';
import { stdin, stdout } from 'node:process';

const rl = readline.createInterface({ input: stdin, output: stdout });

try {
  const name = await rl.question('What is your name? ');
  console.log(`Hello, ${name}!`);
} finally {
  rl.close();
}

The Promise-based approach eliminates callback nesting and integrates naturally with modern JavaScript error handling through try/catch blocks. The finally clause ensures the interface closes even if exceptions occur.

Method 3: Async Iterator for Continuous Input

For applications requiring continuous input processing, the Interface class implements [Symbol.asyncIterator], allowing for await...of loops to consume lines lazily. This pattern is defined in lib/readline.js and is particularly efficient for processing large files or persistent user sessions.

import { createInterface } from 'node:readline';
import { createReadStream } from 'node:fs';

const rl = createInterface({
  input: createReadStream('tasks.txt'),
  crlfDelay: Infinity,
});

for await (const line of rl) {
  console.log(`Task: ${line}`);
}
rl.close();

Setting crllfDelay: Infinity ensures consistent handling of Windows-style line endings (\r\n) across all platforms. This method maintains constant memory usage regardless of input size.

Method 4: Raw Mode and Keypress Events

For character-level input control—such as password masking or real-time menu navigation—readline.emitKeypressEvents enables raw TTY mode. This functionality, implemented in lib/readline.js, bypasses line buffering to emit 'keypress' events for each keystroke.

import { emitKeypressEvents } from 'node:readline';
import { stdin, stdout } from 'node:process';

emitKeypressEvents(stdin);
if (stdin.isTTY) stdin.setRawMode(true);

let password = '';
stdout.write('Enter password: ');

stdin.on('keypress', (char, key) => {
  if (key.name === 'return') {
    stdout.write('\n');
    console.log(`Your password is ${'*'.repeat(password.length)}`);
    stdin.setRawMode(false);
    stdin.pause();
  } else if (key.name === 'backspace') {
    password = password.slice(0, -1);
    stdout.clearLine(0);
    stdout.cursorTo(0);
    stdout.write('Enter password: ' + '*'.repeat(password.length));
  } else if (char) {
    password += char;
    stdout.write('*');
  }
});

Raw mode requires manual handling of special keys including Ctrl+C (SIGINT) and backspace. Always restore TTY state with setRawMode(false) before exiting to prevent terminal corruption.

Key Source Files in the Node.js Repository

Understanding how to take input in node js applications is facilitated by examining the actual implementation in the nodejs/node repository:

  • lib/readline.js: Contains the core Interface class, line buffering logic, and event emission systems.
  • doc/api/readline.md: Official documentation covering all APIs, configuration options, and platform-specific behaviors.
  • test/parallel/test-readline-interface.js: Comprehensive test suite validating edge cases including EOF handling, raw mode transitions, and Unicode input processing.

Summary

  • Use node:readline/promises for modern async/await syntax when processing single questions or sequential prompts.
  • Use node:readline with callbacks for compatibility with Node.js versions prior to v17.0.0 or when working with legacy callback-based architectures.
  • Use async iteration (for await...of) when processing continuous input streams, large files, or persistent CLI sessions to maintain constant memory usage.
  • Use raw mode with emitKeypressEvents when requiring character-level control for password masking, real-time validation, or interactive menus.

Frequently Asked Questions

How do I handle password input without displaying characters on the screen?

Use readline.emitKeypressEvents to enable raw mode on process.stdin, then intercept each 'keypress' event to mask output with asterisks while building the password string in memory. Remember to call stdin.setRawMode(false) before exiting to restore normal terminal behavior.

What is the difference between node:readline and node:readline/promises?

The node:readline module provides a callback-based API where rl.question() accepts a callback function as its second argument. The node:readline/promises submodule, available since Node.js v17.0.0, exports an Interface class where question() returns a Promise, enabling cleaner async/await syntax and better error handling through try/catch blocks.

Can I use the readline module to process files line by line?

Yes. Create a readline.Interface instance with a file stream as the input parameter (using fs.createReadStream), then consume lines using for await (const line of rl). Set crlfDelay: Infinity in the options to handle Windows line endings correctly. This approach processes files lazily without loading the entire contents into memory.

How do I prevent my Node.js script from hanging after collecting user input?

Always call rl.close() after finishing input collection to release the Interface instance and allow the event loop to exit. In promise-based code, place rl.close() in a finally block to ensure execution even if errors occur. For raw mode applications, additionally call stdin.setRawMode(false) and stdin.pause() to restore terminal state and stop listening for events.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →