readlinePromises.createInterface()
Creates a readline interface whose question() and prompt helpers return promises instead of taking callbacks.
readlinePromises.createInterface(options)- Added in Node.js v17.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$options | Object | Same shape as readline.createInterface(). Only input is required. |
$options.input | stream.Readable | The readable stream to split into lines. |
$options.outputoptional | stream.Writable | Where prompts and echoed input are written. |
$options.completeroptional | Function | Tab-completion function. |
$options.terminaloptional | boolean= checks isTTY on output | Whether to emit ANSI/VT100 escape codes and echo input. |
$options.historyoptional | string[]= [] | Initial history lines. |
$options.historySizeoptional | number= 30 | Maximum retained history lines. |
$options.removeHistoryDuplicatesoptional | boolean= false | Remove the older duplicate when a repeated line is added to history. |
$options.promptoptional | string= '> ' | The prompt string used by rl.prompt(). |
$options.crlfDelayoptional | number= 100 | Milliseconds within which \r followed by \n counts as one newline; coerced to a minimum of 100. |
$options.escapeCodeTimeoutoptional | number= 500 | Milliseconds to wait for an ambiguous key sequence. |
$options.tabSizeoptional | integer= 8 | Columns a tab occupies. |
$options.signaloptional | AbortSignal | Aborting it closes the interface. |
Return value
readlinePromises.InterfaceAn Interface whose question() returns a Promise and whose associated Readline class returns promises from commit().
Errors
Throws TypeError if input is missing or is not a readable stream. Calling rl.question() after rl.close() returns a rejected promise.
Changelog
- PHP v24.0.0, v22.17.0The readline/promises API is marked stable (previously experimental).
node:readline/promises is the same interface as node:readline with the callback surface replaced by promises. The options are identical; what changes is that rl.question() resolves with the answer, so a sequence of prompts reads top to bottom instead of nesting.
When to reach for it
- Any interactive script that asks more than one question — installers, scaffolders, migration tools.
- Prompting inside an
asyncfunction where a callback would break the flow. - Adding a timeout to a prompt with
AbortSignal.timeout().
What trips people up
The module has its own specifier. require('node:readline').promises does not exist — import node:readline/promises. The interface it returns is a different class from the callback one, so mixing them is not possible on a single instance.
Only the interface is promisified. readlinePromises.cursorTo() and friends live on a Readline helper class whose operations are queued and flushed by commit(); the standalone readline.cursorTo() function stays in the callback module.
Everything else about the interface still applies: it starts consuming input at construction, question() after close() rejects, and you must close it or the process will not exit.
Example
When to use it
- Ask a series of setup questions in a project scaffolding CLI
- Confirm a destructive migration before running it
- Prompt for a value inside an async function without nesting callbacks
- Time a prompt out after ten seconds with AbortSignal.timeout()
Frequently asked questions
How do I await user input in Node.js?
Import node:readline/promises, create an interface over process.stdin and process.stdout, and await rl.question('...'). The promise resolves with the typed line once the user presses Enter.
What is the difference between node:readline and node:readline/promises?
They expose the same options and the same 'line'/'close' events; the promises version replaces the callback on rl.question() with a returned promise and gives cursor movement a queued Readline class committed with commit(). Import the promises specifier directly — node:readline has no .promises property.
Why does rl.question() reject after I close the interface?
A closed interface no longer manages its input stream, so a new question can never be answered and the promise rejects with ERR_USE_AFTER_CLOSE. Ask every question before calling rl.close(), typically with close() in a finally block.
More examples
A real interactive setup script
Three prompts run in sequence with no nesting because each question() resolves before the next runs. The finally block guarantees the interface is closed even if writeFile throws, so the process exits instead of hanging on stdin.
import readlinePromises from 'node:readline/promises';
import fs from 'node:fs/promises';
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const name = await rl.question('Project name: ');
const port = await rl.question('Port [3000]: ');
const useTs = await rl.question('TypeScript? (y/N) ');
const config = {
name: name.trim(),
port: Number(port.trim() || 3000),
typescript: /^y(es)?$/i.test(useTs.trim()),
};
await fs.writeFile('app.config.json', JSON.stringify(config, null, 2));
console.log('wrote app.config.json');
} finally {
rl.close();
}Timing a prompt out
AbortSignal.timeout() rejects the pending question with an AbortError after ten seconds, which the catch turns into a safe default. With the callback API the same behaviour needs an AbortController plus a manual setTimeout.
import readlinePromises from 'node:readline/promises';
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const answer = await rl.question('Continue? (y/N) ', {
signal: AbortSignal.timeout(10_000),
});
console.log(answer.trim().toLowerCase() === 'y' ? 'continuing' : 'aborted');
} catch (err) {
if (err.name === 'AbortError') console.log('no answer in 10s, aborting');
else throw err;
} finally {
rl.close();
}Looping until the answer validates
A plain for loop with await re-asks until the value passes, which is awkward to express with callbacks because each retry would add another level of nesting. The interface is reused across every question.
import readlinePromises from 'node:readline/promises';
const rl = readlinePromises.createInterface({
input: process.stdin,
output: process.stdout,
});
async function ask(query, validate) {
for (;;) {
const value = (await rl.question(query)).trim();
if (validate(value)) return value;
console.log(' invalid, try again');
}
}
const email = await ask('Email: ', (v) => v.includes('@'));
const age = await ask('Age: ', (v) => /^\d+$/.test(v));
rl.close();
console.log({ email, age: Number(age) });
Discussion