rl.question()

Writes a prompt to the output stream and passes the user's typed answer to a callback.

Signature
rl.question(query[, options], callback)
  • Added in Node.js v0.3.3

Parameters

ParameterTypeDescription
$querystringThe text written to the output stream before waiting for input. It is prepended to the prompt.
$optionsoptionalObjectAdditional options for the question.
$options.signaloptionalAbortSignalAborting it cancels the pending question. The callback is not invoked; listen for 'abort' on the signal instead.
$callbackFunctionCalled as callback(answer) with the user's input as its only argument.

Return value

undefinedNothing. The answer is delivered to callback. In node:readline/promises the same method returns a Promise instead.

Errors

Throws ERR_USE_AFTER_CLOSE if called after rl.close(), and ERR_INVALID_ARG_TYPE if callback is not a function.

rl.question() writes query to the interface's output stream, resumes input if it was paused, and waits for one line. The callback receives that line with the terminator removed — untrimmed, so a stray space the user typed is included.

When to reach for it

  • A single confirmation before a destructive action.
  • Filling in a value the CLI could not infer from flags or environment.
  • Codebases already on the callback API where switching modules is not worth it.

What trips people up

The callback takes only an answer — there is no error argument. Cancelling via options.signal means the callback is simply never called, so attach an 'abort' listener to the signal to handle the timeout branch.

Several questions in a row nest, one callback inside the next. For anything beyond one prompt, use readlinePromises.createInterface() and await rl.question(...) instead.

An interface with no output stream shows no prompt at all, and one whose input has already ended calls back never. After rl.close(), calling question() throws ERR_USE_AFTER_CLOSE.

Nothing is echoed masked: there is no built-in password mode. Hiding input means handling keypresses yourself.

Example

Example · javascript
const readline = require('node:readline');

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

rl.question('Delete the build directory? (y/N) ', (answer) => {
  const confirmed = /^y(es)?$/i.test(answer.trim());
  console.log(confirmed ? 'deleting build/' : 'nothing deleted');
  rl.close();
});

rl.on('close', () => {
  console.log('done');
});

// $ node confirm.js
// Delete the build directory? (y/N) y
// deleting build/
// done

When to use it

  • Ask for a yes/no confirmation before deleting production data
  • Prompt for a value that was not supplied as a command-line flag
  • Cancel a prompt automatically after a timeout using an AbortController
  • Ask a follow-up question only when the first answer requires it

Frequently asked questions

How do I prompt for user input in a Node.js script?

Create an interface over process.stdin and process.stdout with readline.createInterface(), then call rl.question('prompt ', (answer) => { ... }) and close the interface inside the callback. For more than one prompt, use node:readline/promises and await the answers instead.

Why is my rl.question callback never called?

Either the input stream already ended — a piped or closed stdin gives nothing to read — or the question was cancelled through options.signal, in which case the callback is deliberately skipped. Handle the cancelled case by listening for 'abort' on the signal.

Can rl.question hide a password as it is typed?

No, readline has no masked input mode; the answer is echoed like any other line when terminal is true. Hiding it requires putting stdin into raw mode and handling keypress events yourself, or using a dedicated prompt package.

More examples

Cancelling a question with an AbortController

When the signal aborts, the question callback is never invoked — there is no error argument to inspect — so the 'abort' listener is the only place the timeout branch can run. clearTimeout in the success path stops the timer from firing after an answer arrives.

Example · javascript
const readline = require('node:readline');

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

const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 10_000);

ac.signal.addEventListener('abort', () => {
  console.log('\nno answer in 10s, keeping the default');
  rl.close();
}, { once: true });

rl.question('Port to listen on? ', { signal: ac.signal }, (answer) => {
  clearTimeout(timer);
  const port = Number(answer.trim()) || 3000;
  console.log(`listening on ${port}`);
  rl.close();
});

Chained questions, and why it gets deep

Each answer is only available inside its own callback, so three prompts mean three levels of nesting and any shared error handling has to be repeated. The same flow in node:readline/promises is three sequential await lines.

Example · javascript
const readline = require('node:readline');

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

rl.question('Name: ', (name) => {
  rl.question('Email: ', (email) => {
    rl.question('Subscribe to updates? (y/N) ', (sub) => {
      console.log({
        name: name.trim(),
        email: email.trim(),
        subscribed: /^y(es)?$/i.test(sub.trim()),
      });
      rl.close();
    });
  });
});

Promisifying a single question

The wrapper turns the callback into a promise and bridges the abort path into a rejection, which the callback API leaves as a silent no-op. This is exactly what node:readline/promises already does, so prefer that module unless you are stuck on the callback interface.

Example · javascript
import readline from 'node:readline';

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

function ask(query, signal) {
  return new Promise((resolve, reject) => {
    if (signal) {
      signal.addEventListener(
        'abort',
        () => reject(new Error('question aborted')),
        { once: true },
      );
    }
    rl.question(query, { signal }, resolve);
  });
}

try {
  const branch = await ask('Branch to deploy: ', AbortSignal.timeout(30_000));
  console.log(`deploying ${branch.trim()}`);
} catch (err) {
  console.error(err.message);
} finally {
  rl.close();
}

Discussion

  • Be the first to comment on this lesson.