Command-line arguments are useful when a script already has all its input. An interactive tool can instead ask a person for missing information, explain a choice, or require deliberate confirmation before an operation. Node 24's readline promise interface makes sequential questions straightforward, but the tool still needs validation, cancellation, and cleanup rules. This guide focuses on interactive questions rather than repeating log-file line parsing. The complete fixtures use in-memory input and output streams to simulate a terminal conversation without requiring a person to type during verification. The same prompt functions can receive actual terminal streams in a real CLI.
Build one sequential conversation
Create an interface with an input stream and an output stream, then await question for each prompt. The promise resolves with the supplied line. A single interface represents one conversation with its input stream; avoid several independent interfaces competing to read the same terminal. Ask the next question after the previous one resolves. Overlapping questions can make it unclear which answer belongs to which prompt, even though the JavaScript calls look individually valid.
In a real terminal CLI, process.stdin and process.stdout commonly provide the streams. In a test, PassThrough and a writable sink provide controlled input and captured prompts. terminal: false disables terminal-specific behavior for the deterministic fixture. The test writes an answer only after its question has been registered. Preloading several lines before later questions exist can create a misleading test because lines may be consumed before the next question is waiting. Keep the conversation's order explicit rather than relying on buffered input timing.
Validate answers before using them
A question returns a string, not a validated number or an authorized action. Trim whitespace only where the field contract permits it. For a quantity, require the intended decimal syntax, convert it, and enforce a product limit. Number('') produces zero, while parseInt can accept a numeric prefix followed by junk, so neither should define the entire validation rule by itself. A name may have different rules and should not be forced into a numeric validation pattern.
An interactive tool can explain an invalid answer and ask again, but retrying forever is unsuitable for noninteractive automation. Define a maximum number of attempts or a cancellation route when appropriate. Confirmation should identify the operation clearly and require an explicit accepted value. Do not treat any nonempty string as yes. The demonstration keeps all actions in memory and prints a calculated total, so a mistaken answer cannot trigger an actual deployment or deletion. Production tools should perform any final authority checks at the operation boundary as well as in the prompt flow.
Handle EOF, cancellation, and stream ownership
Always close the readline interface when the conversation ends, normally in finally. An open interface and its input can keep a process alive or retain listeners. Closing the interface does not mean every underlying stream belongs to the prompt helper. A function receiving process.stdin should not casually destroy that shared stream if another part of the program still needs it. State which component owns interface creation and cleanup.
question accepts an AbortSignal. Aborting rejects the pending question, allowing the caller to distinguish cancellation from a supplied answer. Handle that rejection and still close the interface. A terminal can also reach end-of-file, and a user can interrupt a tool; decide whether those conditions cancel, use a documented default, or fail with a helpful exit status. Do not silently turn missing input into consent. For server processes, interactive prompting is normally the wrong mechanism: configuration should arrive through an explicit noninteractive interface rather than an indefinite wait on a terminal that may not exist.
Design for terminals and automation separately
A CLI can be used in an interactive terminal, a pipeline, or a continuous-integration job. Detect or configure the expected mode instead of assuming prompts will always be visible. For automation, command-line flags and structured input are often clearer. Keep human-readable prompts separate from machine-readable results so scripts do not have to parse decorative messages. Error output can go to stderr while ordinary results go to stdout when that matches the tool's contract.
Readline does not provide a secure password prompt merely because it asks a question. Ordinary terminal echo can display the typed answer. Do not collect secrets with the plain prompt in this lesson and describe them as hidden. Use an appropriate supported secret-input mechanism and avoid storing sensitive answers in history or logs. Terminal control sequences and user-provided text also deserve care when printing. The fixture uses a sink so its exact output consists only of the program's result lines, while the test can independently assert which prompts were emitted.
Node.js documentation and version notes
Node.js Interactive Readline CLIs Examples
Each example shows the code, the output it produces and what each part does.
Answering readline questions from a controlled stream
Save the fixture as a .mjs file and run it on Node 24. It creates one interface, registers a name question, supplies Ada through the input stream, then repeats that pattern for a quantity. The numeric validator accepts only one to ten. The program prints the name, quantity, and total, and reports whether both expected prompts appeared in its captured output. The helper simulation does not replace readline with a mock; actual question promises consume lines from the controlled stream. Cleanup closes the interface and ends the local input stream even if validation fails.
import { createInterface } from 'node:readline/promises';
import { PassThrough, Writable } from 'node:stream';
const input = new PassThrough();
let prompts = '';
const output = new Writable({ write(chunk, encoding, done) { prompts += chunk; done(); } });
const rl = createInterface({ input, output, terminal: false });
async function answer(prompt, value) {
const pending = rl.question(prompt);
input.write(value + '\n');
return pending;
}
try {
const name = (await answer('Name? ', 'Ada')).trim();
const raw = await answer('Quantity? ', '3');
if (!/^[1-9]\d*$/.test(raw) || Number(raw) > 10) throw new Error('Invalid quantity');
const quantity = Number(raw);
console.log(name, quantity, quantity * 8);
console.log(prompts === 'Name? Quantity? ');
} finally { rl.close(); input.end(); }Ada 3 24
trueNode.js Interactive Readline CLIs in Real Projects
Build a yes-only confirmation prompt with cancellation
Create a confirmation prompt that accepts only the word yes after trimming and case normalization. Supply NO to prove that a nonempty answer is not automatic approval. Then begin a second question and cancel it using an AbortController. The solution prints the confirmation decision and the cancellation error name, with one cleanup owner for the interface. This demonstrates two distinct outcomes: the user explicitly declined one question, and no answer was supplied for the canceled question. Keep that difference visible in a real tool's result rather than treating both as successful consent or an unexplained failure.
import { createInterface } from 'node:readline/promises';
import { PassThrough, Writable } from 'node:stream';
const input = new PassThrough();
const output = new Writable({ write(chunk, encoding, done) { done(); } });
const rl = createInterface({ input, output, terminal: false });
try {
const confirmation = rl.question('Type yes to continue: ');
input.write('NO\n');
console.log((await confirmation).trim().toLowerCase() === 'yes');
const controller = new AbortController();
const pending = rl.question('Another answer? ', { signal: controller.signal });
controller.abort();
try { await pending; }
catch (error) { console.log(error.name); }
} finally { rl.close(); input.end(); }false
AbortErrorNode.js Interactive Readline CLIs FAQ
Why not write all test answers before asking questions?
A line can arrive before the intended question is registered, making a later prompt miss that answer. Register the question first, then supply its line, then await completion before starting the next step. This makes the test's conversation order explicit. The fixture exercises real readline behavior with controlled streams rather than returning canned values from a replaced question function.
Does closing readline destroy process.stdin?
Closing the interface releases its conversation behavior, but stream ownership should remain an explicit design choice. A prompt helper should not destroy a caller's shared input merely because its own question sequence finished. In these fixtures, the helper created the PassThrough stream and can end it safely. In a larger CLI, keep interface and stream cleanup coordinated with other consumers and the intended shutdown policy.
Can an automated job answer an interactive prompt?
It can supply input, but interactive prompting often produces fragile automation when defaults, ordering, or EOF behavior are unclear. Provide flags or structured input for unattended use and document missing-value behavior. If a required confirmation is absent, fail rather than silently approving. Keep prompts out of machine-readable output where possible. A noninteractive mode should be an intentional interface, not an accidental side effect of piped terminal input.
Is a readline password question hidden?
Not by default. A plain terminal prompt can echo the user's characters, and test capture or logging can expose answers. Use a suitable supported secret-input mechanism if a CLI truly needs to collect credentials. Prefer established credential flows where available. Do not retrofit a normal question by merely changing its label to Password and then claim the input is protected.
Node.js Interactive Readline CLIs Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What decision does the worked confirmation produce for NO?
Node.js Interactive Readline CLIs Quiz
2 questions to check what you learned.
Why await each question before starting the next?
- It keeps answer ownership and conversation order explicit
- It converts every answer automatically into a number
- It hides typed characters from the terminal
Where should readline cleanup normally happen?
- In finally under the component that owns the interface
- Only after a valid numeric answer
- Inside every data callback by destroying process.stdin