readline-sync
readline-sync reads console input synchronously. You write const name = readlineSync.question('Name? ') and the next line of your script runs only after the user presses Enter, with no callback and no await. On top of that it stacks a large set of helpers: masked password entry, single-key input without Enter, yes or no prompts, numbered menus, typed input for integers, floats, email addresses and file paths, and shell-style command loops with history. It has no dependencies and works even when stdin or stdout has been redirected, falling back to an external program when the platform needs one.
For a script where blocking is genuinely fine, this is still the most convenient way to ask a question in Node, and the helper set is broader than anything comparable. For anything long-lived, concurrent or automated, it is the wrong shape, and it has been archived since 2022 with no release since 2019.
Use it if
- You are writing a small script or teaching example where blocking is fine and callbacks or async would obscure what the code is doing
- You need one of its ready-made prompts, such as keyInSelect for a numbered menu or questionNewPassword for confirm-and-validate, and do not want to assemble them yourself
- You want masked password entry from a single function call with hideEchoBack, without wiring raw mode on stdin by hand
- You are maintaining an existing CLI already built on it and need to understand the option set before changing anything
- Anything else has to happen while you wait, because this blocks the whole event loop: no timers fire, no sockets are serviced, no promises resolve until the user answers
- Your program ever runs unattended, since the README says an error is thrown when the platform cannot read from a TTY interactively, so CI and piped stdin fail rather than fall back
- You are inside a server, a worker or anything with an open connection, where freezing the process for the length of a human's typing is not acceptable
- You want it maintained: the repository has been archived since November 2022 and the last publish was 1.4.10 in July 2019
- You need TypeScript or ESM out of the box, since 1.4.10 is CommonJS with no bundled declarations; types live in a separate DefinitelyTyped package
- You want colours or cursor control in your prompts, because the README warns that ANSI escape sequences may be ignored depending on the terminal and are not recommended if you support more environments
Setup reality
Install and first prompt take about a minute: no dependencies, no native build, no config, and the engines field claims node >= 0.8.0. What you should understand before committing is how it achieves synchronous reads. Node has no synchronous console read, so readline-sync gets one by running an external program and blocking on it, and on very old Node versions without synchronous process execution it falls back to what the author calls piping via files. The README is candid that this blocks the event loop and the process and may make your script slow, and explains the choice: native addons for synchronous input do not compile everywhere, and the alternatives the author looked at did not protect the data. So the cost is by design, and it is total. While a question is pending, nothing else in your process runs, which rules out servers, background timers, progress spinners, and any concurrent work. The second thing to plan for is non-interactive execution. In an environment that cannot read a TTY interactively, calls throw rather than returning a default, so every prompt in a script that might run in CI needs either a try/catch or a check that stdin is a TTY before you get there. Design the flow so flags can supply every answer and prompting is the fallback, not the only path. Beyond that the option system is large but consistent: options passed to a method override defaults you set once with setDefaultOptions, and the important ones are hideEchoBack with mask for secrets, limit with limitMessage to reject unexpected input, defaultInput for Enter-only replies, trueValue and falseValue to coerce answers to booleans, and keepWhitespace to stop trimming. Placeholders such as $<lastInput> and the range form $<1-6> appear inside limit and message strings, which is powerful and is also the part people misread first. Masking is best effort: the README notes that in some cases, such as redirected input on Windows, the mask character you asked for may not be the one used.
Patterns
Read one line and continueask-a-question
const readlineSync = require('readline-sync')
const name = readlineSync.question('May I have your name? ')
console.log(`Hi ${name}!`)Execution stops on this line until Enter is pressed. Nothing else in the process runs meanwhile, including timers already scheduled.
Read a password without echoing ithide-secret-input
const readlineSync = require('readline-sync')
const password = readlineSync.question('PASSWORD: ', {
hideEchoBack: true,
mask: '*',
})The README warns that in some environments, such as redirected input on Windows, the mask actually used may differ from the one requested. Never log the result.
Survive CI and piped inputguard-non-interactive
const readlineSync = require('readline-sync')
function ask (query, fallback) {
if (!process.stdin.isTTY) return fallback
try {
return readlineSync.question(query, { defaultInput: fallback })
} catch (err) {
return fallback
}
}
const env = ask('Environment? ', process.env.DEPLOY_ENV || 'staging')Prompts throw when the platform cannot read a TTY interactively. Check isTTY first and keep a flag or environment variable as the real source of truth.
Confirm without pressing Entersingle-key-yes-no
const readlineSync = require('readline-sync')
if (readlineSync.keyInYN('Delete every build artefact?')) {
removeArtefacts()
} else {
console.log('Left alone.')
}keyInYN treats any key other than Y as no, which makes a stray keystroke a silent decline. Use keyInYNStrict when the answer must be deliberate.
Show a numbered menuchoose-from-list
const readlineSync = require('readline-sync')
const targets = ['staging', 'canary', 'production']
const index = readlineSync.keyInSelect(targets, 'Deploy where?')
if (index === -1) return console.log('Cancelled.')
console.log(`Deploying to ${targets[index]}`)The cancel entry returns -1, not undefined. Check for it explicitly or you will index the array with -1 and get undefined.
Reject answers that are not on the listrestrict-input
const readlineSync = require('readline-sync')
const colour = readlineSync.question('Which signal colour? ', {
limit: ['green', 'yellow', 'red'],
limitMessage: '$<lastInput> is not a signal colour.',
})limit accepts strings, numbers, regular expressions, functions or arrays of those, and re-asks until one matches. $<lastInput> in the message is replaced with what was typed.
Ask for a number, an email or a pathtyped-answers
const readlineSync = require('readline-sync')
const port = readlineSync.questionInt('Port? ')
const ratio = readlineSync.questionFloat('Ratio? ')
const email = readlineSync.questionEMail('Contact address? ')
const file = readlineSync.questionPath('Config file? ', { isFile: true })These loop until the input parses, so they never return an invalid value and never return control either. There is no attempt limit unless you add one.
Apply options to every later promptset-defaults-once
const readlineSync = require('readline-sync')
readlineSync.setDefaultOptions({ prompt: '$ ', keepWhitespace: true })
const a = readlineSync.prompt() // uses the defaults
const b = readlineSync.prompt({ keepWhitespace: false }) // overridden onceDefaults persist for the life of the process. Per-call options override them without changing them, which is the usual source of confusion in long scripts.
Run a small shell-style loopcommand-loop
const readlineSync = require('readline-sync')
readlineSync.promptCLLoop({
add: (target, into) => console.log(`${target} added to ${into}`),
remove: (target) => console.log(`${target} removed`),
bye: () => true,
})
console.log('Exited')The loop ends when a handler returns true. Unknown commands print a built-in message rather than reaching your code, so there is no catch-all handler.
Accept a default when the user just presses Enterdefault-on-enter
const readlineSync = require('readline-sync')
const branch = readlineSync.question('Branch [main]: ', {
defaultInput: 'main',
})Empty input returns defaultInput, so put the default in the prompt text too. It applies only to question and prompt methods, not to the keyIn family.
Coerce free text into true or falseboolean-answers
const readlineSync = require('readline-sync')
const enabled = readlineSync.question('Enable telemetry? ', {
trueValue: ['y', 'yes', 'on'],
falseValue: ['n', 'no', 'off'],
limit: ['y', 'yes', 'on', 'n', 'no', 'off'],
})
console.log(typeof enabled) // 'boolean'Without limit, anything not on either list is returned as the raw string, so the type varies. Pair the two options or check the type before using it.
Replace it with core readline when you need the loop freemigrate-to-async
const readline = require('node:readline/promises')
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
try {
const name = await rl.question('May I have your name? ')
console.log(`Hi ${name}!`)
} finally {
rl.close()
}Built into Node, no dependency, and the event loop keeps running while you wait. You give up the helper prompts, which is what @inquirer/prompts or enquirer replace.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| prompt-sync | npm | You genuinely need blocking input and want a much smaller surface than readline-sync's forty-odd methods and options |
| @inquirer/prompts | npm | You can use async/await and want maintained, composable prompts with validation, choices and keyboard navigation |
| enquirer | npm | You want a wide set of prompt types with a small dependency footprint and are fine with a promise-based API |
| prompts | npm | You want a minimal promise-based prompt library that is easy to drive from a plain array of question objects |