mrkeyoor.com_
Sat 08 Aug 22:50 UTC
npmCLI & Toolingupdated 08 Aug 2026

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.

Verdict

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.

API stability5/5The method names and option semantics have been fixed since the 1.4.x line settled around 2016, and the last publish, 1.4.10 in July 2019, was maintenance rather than change. Methods dropped along the way were moved into a separate README-Deprecated.md instead of being removed outright, so old code keeps working. With the repository archived there is no further churn possible. The practical risk is not the API moving, it is the environment moving underneath a package that reaches for external programs.
Docs4/5The README is long and genuinely useful: every method and option documented with runnable examples, console transcripts showing what the user sees, animated captures for the interactive cases, and a quick-start section organised by what the user does rather than by function name. It is also honest in the Note section about platform differences, ANSI escape sequences being unreliable, and the external-program approach blocking the process. Weak points are navigation, since it is one enormous page, and the absence of any guidance on non-interactive environments beyond one try/catch snippet.
Maintenance1/5The repository has been archived since 2022-11-03 and is read-only, with 806 stars, 62 forks and no open issues because nothing can be opened. The last npm publish was 1.4.10 on 2019-07-27. The README's own opening line points out that ECMAScript now has Promise, await and async, which reads as the author's view that the synchronous approach has been overtaken. Nothing is broken today, but no fix is coming if a future Node or terminal change breaks the external-program path.
Ecosystem3/5Roughly 4.4 million weekly downloads, split between direct use in scripts and tutorials and transitive use in older generators and scaffolding tools. TypeScript users have @types/readline-sync on DefinitelyTyped, last published in 2023, which covers the API well. There are no plugins or theme systems, and nothing else integrates with it, because a blocking call is hard to compose. The modern prompt ecosystem, including Inquirer, enquirer and prompts, is entirely async and shares no interfaces with this.

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
Skip it if

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 once

Defaults 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

PackageRegistryPick it when
prompt-syncnpmYou genuinely need blocking input and want a much smaller surface than readline-sync's forty-odd methods and options
@inquirer/promptsnpmYou can use async/await and want maintained, composable prompts with validation, choices and keyboard navigation
enquirernpmYou want a wide set of prompt types with a small dependency footprint and are fine with a promise-based API
promptsnpmYou want a minimal promise-based prompt library that is easy to drive from a plain array of question objects