Skip to content
Blog

Read Input From the Terminal in Node.js with readline

By Published A short read

To read input from the terminal in Node.js, use the built-in node:readline/promises module. Create an interface on process.stdin and process.stdout, then await rl.question('What is your name? ') waits for the user to type a line and press Enter. Call rl.close() when you're done, or the program never exits.

In this post we ask one question, then several, keep asking until the user types exit, and read text piped in from a file or another command. Along the way we fix the two bugs that catch almost everyone: a program that never ends, and answers that vanish when the input is piped.

Key takeaways

  • import * as readline from 'node:readline/promises', then await rl.question(...) gives back what the user typed, as a string.
  • Always call rl.close() at the end. Until you do, Node.js keeps waiting for input and the program doesn't exit.
  • Answers are strings. Turn them into numbers with Number(answer), and check for NaN and for an empty answer.
  • rl.question() is made for a person typing. When answers are piped in all at once, the second answer can be lost. A small ask() built on readline's async iterator works both ways.
  • For piped input or a file, for await (const line of rl) gives you one line at a time, and for await (const chunk of process.stdin) reads all of it.

Our 2022 Short shows the original callback version in under a minute. It still works. This post adds the modern await version and the parts the Short skips.

How to read a line of input with readline/promises

Let's start with one question. Make a file called ask.mjs:

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

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

Run it and type a name:

$ node ask.mjs
What is your name? Asha
Hello, Asha!

Line by line:

  • node:readline/promises is built into Node.js, so there's nothing to install. The node: prefix marks it as a core module.
  • createInterface({ input, output }) reads lines from the keyboard (process.stdin) and writes the questions to the screen (process.stdout).
  • rl.question() prints the question, waits for Enter, and gives back the line as a string, without the newline. await pauses the program until that happens.
  • rl.close() lets go of the keyboard so the program can finish.
The .mjs ending makes the file an ES module, and that's what allows import and await at the top level of the file. In a project, you can add "type": "module" to package.json and keep the .js ending.

The same thing with require

If your project uses require (CommonJS), wrap the code in an async function, because a CommonJS file can't use await at the top level. Save this as ask.cjs:

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

async function main() {
  const rl = readline.createInterface({ input, output });
  const name = await rl.question('What is your name? ');
  console.log(`Hello, ${name}!`);
  rl.close();
}

main();

And here's the version from our 2022 post and Short, with a callback instead of await. It still works in Node.js 24, and it's what you'll see in older tutorials. Save it as ask-callback.js:

const readline = require('node:readline');

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

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

All three print the same thing. Use the await version for new code: with callbacks, every extra question goes one level deeper inside the one before it.

Ask several questions and read a number

Each await waits for its own answer, so questions simply follow one another. Save this as age.mjs:

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

const rl = readline.createInterface({ input, output });
const name = await rl.question('What is your name? ');
const age = Number(await rl.question('How old are you? '));
rl.close();

if (Number.isNaN(age)) {
  console.log(`Sorry ${name}, that isn't a number.`);
} else {
  console.log(`Hi ${name}, next year you'll be ${age + 1}.`);
}
$ node age.mjs
What is your name? Asha
How old are you? 14
Hi Asha, next year you'll be 15.

Everything typed arrives as a string, and "14" + 1 is "141". So we turn the answer into a number with Number() first. If someone types fourteen, Number("fourteen") is NaN (not a number), and the program says Sorry Asha, that isn't a number.

Why do answers go missing when input is piped?

Typing works. Now give the program both answers at once from a file, the way a test script or another program would. answers.txt has two lines, Asha and 14:

node age.mjs < answers.txt

It prints both questions and then stops with Warning: Detected unsettled top-level await and exit code 13. It never says hi.

Here's why. Piped input arrives in big chunks, so both lines reach readline together and it splits them into lines straight away. Asha answers the first question. 14 comes out before the second question has been asked, so no question is waiting for it, and it's lost. The second question then waits for a line that never comes, the input ends, and Node.js gives up: the Node.js docs say that if a top-level await never finishes, the process exits with status code 13.

The fix is to keep every line as it arrives and hand them out one at a time. readline's async iterator, rl[Symbol.asyncIterator]() (the same thing for await uses behind the scenes), does exactly that: it holds lines in a queue until you ask for the next one. Here's a small ask() built on it, in age-ask.mjs:

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

const rl = readline.createInterface({ input, output });
const lines = rl[Symbol.asyncIterator]();

async function ask(question) {
  rl.setPrompt(question);
  output.write(question);
  const { value, done } = await lines.next();
  return done ? null : value;
}

const name = await ask('What is your name? ');
const age = Number(await ask('How old are you? '));
rl.close();

if (Number.isNaN(age)) {
  console.log(`Sorry ${name}, that isn't a number.`);
} else {
  console.log(`Hi ${name}, next year you'll be ${age + 1}.`);
}
  • lines.next() gives the next line from the queue, or waits for one. When the input has ended it says done, and ask() returns null.
  • output.write(question) prints the question. rl.setPrompt(question) tells readline about it too: when you press Backspace or an arrow key, readline redraws the line with the prompt it knows. Leave it out and the redrawn line starts with readline's default prompt, > , instead of your question.
  • Create the iterator straight after createInterface. The Node.js docs warn that other asynchronous work in between can lose lines.

Typed, it looks exactly like age.mjs. Piped, the answers aren't shown on screen (nobody typed them), so the questions run together on one line, but nothing is lost:

$ node age-ask.mjs < answers.txt
What is your name? How old are you? Hi Asha, next year you'll be 15.
The < redirect works in Command Prompt, Git Bash, macOS and Linux. PowerShell doesn't have it: use Get-Content answers.txt | node age-ask.mjs there.

How to keep asking until the user types exit

A loop around ask() makes a tiny interactive program. This one keeps a running total. Save it as total.mjs:

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

const rl = readline.createInterface({ input, output });
const lines = rl[Symbol.asyncIterator]();

async function ask(question) {
  rl.setPrompt(question);
  output.write(question);
  const { value, done } = await lines.next();
  return done ? null : value;
}

let total = 0;
while (true) {
  const line = await ask('> ');
  if (line === null || line.trim() === 'exit') break;
  const n = Number(line);
  if (line.trim() === '' || Number.isNaN(n)) {
    console.log(`"${line}" is not a number`);
  } else {
    total += n;
    console.log(`total: ${total}`);
  }
}
rl.close();
console.log(`Final total: ${total}`);
$ node total.mjs
> 5
total: 5
> 10
total: 15
> ten
"ten" is not a number
> 2.5
total: 17.5
> exit
Final total: 17.5

There are two ways out: type exit, or press Ctrl+D at an empty prompt. Ctrl+D ends the input, readline closes, ask() returns null and the loop stops, so the final total still prints. Because ask() handles the end of the input, this program also works with piped input that has no exit line at all.

How to read piped input and files line by line

When nobody is typing, you don't need questions. Loop over the lines with for await and the loop ends by itself when the input ends. Save this as sum-lines.mjs:

import * as readline from 'node:readline';
import { stdin as input } from 'node:process';

const rl = readline.createInterface({ input, crlfDelay: Infinity });
let count = 0;
let total = 0;

for await (const line of rl) {
  if (line.trim() === '') continue;
  count += 1;
  total += Number(line);
}
console.log(`${count} numbers, total ${total}`);

There's no output this time, so nothing is printed while reading. for await works with either import; we use plain node:readline here because we never call question(). crlfDelay: Infinity makes a Windows line ending (\r\n) always count as one line break, as in the Node.js docs' own example. Blank lines are skipped. With a file, numbers.txt, holding 12, 7 and 30 on three lines, or with another program's output piped in:

$ node sum-lines.mjs < numbers.txt
3 numbers, total 49
$ node -e "for (let i = 1; i <= 100; i++) console.log(i)" | node sum-lines.mjs
100 numbers, total 5050

This version trusts that every line is a number. If one isn't, the total becomes NaN; add the check from total.mjs if your input might contain words.

Read everything at once with process.stdin

Sometimes you want the whole input as one string: a JSON file, or a document to count words in. process.stdin is a stream, so for await works on it directly. Save this as count-words.mjs:

import { stdin } from 'node:process';

if (stdin.isTTY) {
  console.log('Pipe some text in, for example: node count-words.mjs < notes.txt');
  process.exit(1);
}

let text = '';
stdin.setEncoding('utf8');
for await (const chunk of stdin) text += chunk;

const words = text.split(/\s+/).filter(Boolean);
console.log(`${words.length} words`);
$ echo to be or not to be | node count-words.mjs
6 words

stdin.isTTY is true when the program is run straight from a terminal with nothing piped in, and undefined when the input comes from a pipe or a file. Without the check, running node count-words.mjs on its own would sit waiting for you to type something and end the input (Ctrl+D, or Ctrl+Z then Enter on Windows), which looks like it has frozen.

Which one should you use? readline when you want lines (a list of numbers, a log file), and process.stdin when you want the whole text at once.

Common mistakes

The program never ends

You forgot rl.close(). The Node.js docs are direct about it: "the Node.js application will not terminate until the readline.Interface is closed because the interface waits for data to be received on the input stream." Close it as soon as you've read your last answer.

An empty answer turns into 0

Number("") and Number(" ") are both 0, not NaN. So if someone just presses Enter, Number.isNaN() doesn't catch it. Check answer.trim() === '' first, as total.mjs does.

Asking after closing

A question asked after rl.close() doesn't wait for input. The docs say it "returns a rejected promise", and the error code is ERR_USE_AFTER_CLOSE. Ask everything first, then close.

Looking for prompt()

Browsers have a prompt() function that pops up a box. Node.js doesn't: in Node.js 24, typeof prompt is "undefined". readline is the built-in way to ask.

Work these out first, then run them to check.

1) nums.txt has three lines: 3, 4 and exit. What does node total.mjs < nums.txt print?
2) You run age.mjs, type Asha, then just press Enter at "How old are you?". What does it print, and why?
3) What does node -e "console.log(1); console.log(2)" | node sum-lines.mjs print?

Show the answers

1) Three lines: > total: 3, > total: 7 and > Final total: 7. Each prompt sits in front of the next output because the piped answers aren't shown on screen.
2) Hi Asha, next year you'll be 1. The empty answer is "", and Number("") is 0, not NaN. That's the "empty answer" mistake above.
3) 2 numbers, total 3.

Questions people ask

How do I read user input from the terminal in Node.js?

Use the built-in readline module. import * as readline from 'node:readline/promises', create an interface with readline.createInterface({ input: process.stdin, output: process.stdout }), then const answer = await rl.question('Your question? '). Call rl.close() when you've finished asking.

What's the difference between readline and readline/promises?

They're two versions of the same API. In node:readline, rl.question() takes a callback. In node:readline/promises, it returns a Promise you can await. The promises version was added in Node.js 17.0.0 and marked stable in 24.0.0 and 22.17.0.

How do I read a number from the terminal in Node.js?

Read the line as a string, then convert it: const n = Number(await rl.question('Number? ')). Check Number.isNaN(n) for answers that aren't numbers, and check for an empty answer first, because Number("") is 0.

Why doesn't my Node.js program exit after reading input?

The readline interface is still open and waiting for more input. Call rl.close() after your last question. In a for await loop over rl, the loop ends on its own when the input ends, and leaving it with break closes the interface.

How do I read piped input (stdin) in Node.js?

For lines, use for await (const line of rl) on a readline interface over process.stdin. For the whole input as one string, use for await (const chunk of process.stdin) text += chunk. Check process.stdin.isTTY to tell typed input from piped input.

Why does my second question get no answer when I pipe input?

Piped input arrives all at once. The second line comes in before the second question is asked, and rl.question() only receives lines while it's waiting, so that line is dropped. Read lines through readline's async iterator instead, like the ask() function in this post.

Keep going

Checked on 20 October 2026 against the 4 sources below.

Sources

  • Node.js documentation: Readline (the promises API, rl.question(), the async iterator, the 'close' event, crlfDelay)
  • Node.js documentation: ECMAScript modules (the .mjs extension, top-level await and exit code 13)
  • Node.js documentation: process (process.stdin)
  • Node.js documentation: TTY (how Node.js detects a terminal)

Every program in this post was run in Node.js 24.20.0 before publishing, both with answers typed one at a time and with input piped in.