jsguides

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

OptionTypeDefaultDescription
inputStreamprocess.stdinThe readable stream to read from
outputStreamprocess.stdoutThe writable stream to write to
terminalbooleanfalseWhether to treat input/output as a TTY
promptstring> The prompt string
crlfDelaynumber100Delay between CR and LF detection
removeHistoryDuplicatesbooleanfalseRemove 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

MethodDescription
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