readline module
The readline module provides an interface for reading data from a readable stream one line at a time. It is essential for building interactive command-line interfaces (CLIs) in Node.js.
Installation
The readline module is a built-in Node.js module. No installation is required:
const readline = require('readline');
Importing readline gives you access to the API, but you still need to wire it to actual streams before it can do anything. That is what createInterface() handles — it links the module to process.stdin and process.stdout (or any readable/writable pair) and returns an object you can use to ask questions and listen for responses.
Creating an Interface
The primary function is readline.createInterface(), which creates an interface for reading from streams:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
input | Stream | process.stdin | The readable stream to read from |
output | Stream | process.stdout | The writable stream to write to |
terminal | boolean | false | Whether to treat input/output as a TTY |
prompt | string | > | The prompt string |
crlfDelay | number | 100 | Delay between CR and LF detection |
removeHistoryDuplicates | boolean | false | Remove duplicates from history |
Reading Input
The question Method
The simplest way to get input:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('What is your name? ', (answer) => {
console.log(`Hello, ${answer}!`);
rl.close();
});
rl.question() is convenient for single prompts, but it fires a callback once and stops. When you need to handle many lines — reading from a file or building a REPL — use the line event instead. Each newline-terminated chunk that arrives on the input stream triggers a line callback, giving you line-at-a-time access without buffering the entire input.
The line Event
For continuous line-by-line reading, use the line event:
const readline = require('readline');
const fs = require('fs');
const rl = readline.createInterface({
input: fs.createReadStream('file.txt'),
crlfDelay: Infinity
});
rl.on('line', (line) => {
console.log(`Line: ${line}`);
});
rl.on('close', () => {
console.log('File reading complete');
});
Interface Methods
| Method | Description |
|---|---|
rl.question(query, callback) | Prompts the user with a question |
rl.pause() | Pauses the input stream |
rl.resume() | Resumes the input stream |
rl.close() | Closes the interface |
rl.write(data) | Writes to the output |
rl.setPrompt(prompt) | Sets the prompt string |
rl.prompt() | Writes the prompt to the output |
Events
The readline interface emits several events.
line Event
Emitted whenever the input stream receives a line break:
rl.on('line', (line) => {
console.log(`Received: ${line}`);
});
The line event fires for each complete line read. Once the input stream ends — either because the file has been fully consumed or rl.close() was called — the close event fires. Use it for cleanup: flushing buffers, writing summaries, or releasing file handles.
close Event
Emitted when the interface is closed:
rl.on('close', () => {
console.log('Interface closed');
});
Beyond the core line/close cycle, the interface emits flow-control events that let you pause and resume reading. pause fires when the stream is temporarily suspended — useful for rate-limiting or waiting on an async operation. resume fires when reading starts again.
pause and resume Events
rl.on('pause', () => {
console.log('Readline paused');
});
rl.on('resume', () => {
console.log('Readline resumed');
});
Flow events help you track the interface state. Signal events add terminal-style shortcuts — SIGINT for Ctrl+C interrupts and SIGCONT for resume-after-suspend. Handing SIGINT lets you prompt the user before quitting, which prevents accidental exits during long input sessions.
Signal Events
The interface emits signals similar to terminal signals:
// Handle Ctrl+C
rl.on('SIGINT', () => {
rl.question('Are you sure you want to exit? ', (answer) => {
if (answer.match(/^y(es)?$/i)) {
rl.close();
}
});
});
// Handle process resume (after Ctrl+Z suspend)
rl.on('SIGCONT', () => {
console.log('Suspended');
});
The event snippets above show individual listener patterns. A real CLI ties them all together — a menu loop, input handlers, event cleanup, and graceful shutdown. The next example combines these pieces into a working task manager.
Practical Example
Here is a complete example of an interactive CLI:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true
});
const tasks = [];
function showMenu() {
console.log('\n--- Task Manager ---');
console.log('1. Add task');
console.log('2. List tasks');
console.log('3. Quit');
rl.question('Choose an option: ', handleInput);
}
function handleInput(choice) {
switch (choice.trim()) {
case '1':
rl.question('Enter task: ', (task) => {
tasks.push(task);
console.log(`Added: ${task}`);
showMenu();
});
break;
case '2':
console.log('\nYour tasks:');
tasks.forEach((t, i) => console.log(`${i + 1}. ${t}`));
showMenu();
break;
case '3':
console.log('Goodbye!');
rl.close();
break;
default:
console.log('Invalid option');
showMenu();
}
}
rl.on('close', () => {
process.exit(0);
});
showMenu();
The task manager example uses nested callbacks — each menu choice calls rl.question() with another callback, creating a chain that can become hard to follow. Wrapping rl.question() in a Promise flattens this structure, so you can write linear async/await code that reads top to bottom.
Using readline with async/await
For cleaner code, wrap readline in a promise:
const readline = require('readline');
function createInterface(options) {
const rl = readline.createInterface(options);
return {
rl,
question: (query) => new Promise((resolve) => {
rl.question(query, resolve);
})
};
}
async function main() {
const { rl, question } = createInterface({
input: process.stdin,
output: process.stdout
});
const name = await question('What is your name? ');
const age = await question('How old are you? ');
console.log(`Hello, ${name}! You are ${age} years old.`);
rl.close();
}
main();
The wrapper shown above is a manual shim — it works, but it is boilerplate you have to carry into every project. Node.js 17 introduced a built-in alternative that eliminates the wrapping entirely.
readline vs readline/promises
Node.js 17 added readline/promises, which provides a Promise-based API instead of callbacks. The rl.question() method in readline/promises returns a Promise, making it cleaner to use with async/await without wrapping callbacks manually:
import { createInterface } from "readline/promises";
const rl = createInterface({ input: process.stdin, output: process.stdout });
const name = await rl.question("What is your name? ");
rl.close();
console.log(`Hello, ${name}!`);
This is the preferred approach for new code. The callback-based require("readline") is still available for compatibility with older Node.js versions.
Closing the interface
Always call rl.close() when you are finished reading. If you do not close the interface, the Node.js process will not exit because process.stdin keeps the event loop alive. Closing the interface signals that no more input is expected and releases stdin. The close event fires after the interface is closed, which is useful for cleanup.
Reading files line by line
The readline module is efficient for reading large files line by line because it processes one line at a time rather than loading the entire file into memory. Pipe a file read stream into the interface’s input option and listen to the line event. This pattern is memory-efficient for log processing, CSV parsing, and other line-oriented file formats. The close event fires when the stream ends, signaling that all lines have been processed.
See Also
- fs module — File system operations
- process — Process information and control
- events module — Event-driven architecture