O Readline Promises no Node.js oferece uma interface baseada em Promises para ler entrada do terminal, fazer perguntas, processar linhas de arquivos e controlar o cursor. Ele pertence ao módulo node:readline/promises e simplifica CLIs que antes precisavam envolver callbacks manualmente.
A API funciona sobre streams de entrada e saída. Em terminais interativos, ela suporta prompt, histórico, sinais, edição e recursos de cursor. Em arquivos ou pipes, pode ser usada com iteração assíncrona para processar conteúdo linha por linha com baixo consumo de memória.
Neste guia, você aprenderá a criar uma interface, usar question(), AbortSignal, histórico, validação, senhas, iteração assíncrona, arquivos grandes, cursor, cleanup, testes e segurança.
O que é node:readline/promises?
É a versão baseada em Promises do módulo Readline. A documentação oficial de Readline Promises descreve classes e métodos. A documentação oficial de TTY explica terminais, modos e dimensões.
Para a API tradicional, consulte Readline no Node.js. Para terminal, veja TTY no Node.js. O artigo de Console no Node.js ajuda a separar stdout e stderr.
Importando
const readline = require('node:readline/promises');
const {
stdin: input,
stdout: output
} = require('node:process');Criando a interface
const rl = readline.createInterface({
input,
output
});Feche a interface ao terminar para liberar listeners e permitir o encerramento do processo.
Pergunta básica
try {
const name = await rl.question('Seu nome: ');
console.log(`Olá, ${name}!`);
} finally {
rl.close();
}question() resolve com o texto digitado sem a quebra de linha.
Validação
async function askRequired(question) {
while (true) {
const answer = (await rl.question(question)).trim();
if (answer.length > 0) {
return answer;
}
output.write('Valor obrigatório.\n');
}
}Defina limite de tentativas quando a CLI roda em automação.
Números
async function askInteger(question) {
const answer = await rl.question(question);
const value = Number(answer);
if (!Number.isInteger(value)) {
throw new Error('Número inteiro inválido');
}
return value;
}Não use parseInt sem verificar o restante da string quando o formato precisa ser estrito.
Confirmação
async function confirm(question) {
const answer = (await rl.question(
`${question} [s/N] `
)).trim().toLowerCase();
return answer === 's' || answer === 'sim';
}Use padrão seguro para operações destrutivas: ausência de resposta deve significar cancelamento.
Timeout com AbortSignal
const signal = AbortSignal.timeout(30000);
try {
const answer = await rl.question(
'Resposta: ',
{ signal }
);
} catch (error) {
if (error.name === 'AbortError') {
console.error('Tempo esgotado');
} else {
throw error;
}
}O suporte depende da versão. Timeout evita que jobs automatizados aguardem para sempre.
Cancelamento externo
const controller = new AbortController();
process.once('SIGINT', () => {
controller.abort();
});
await rl.question('Continuar? ', {
signal: controller.signal
});Trate Ctrl+C e encerre com código apropriado.
Sinais
A interface pode emitir eventos relacionados a SIGINT e outros sinais em terminais compatíveis. Não presuma o mesmo comportamento em Windows, pipes e ambientes CI.
terminal
const rl = readline.createInterface({
input,
output,
terminal: output.isTTY
});Quando stdout não é TTY, desabilitar recursos interativos evita códigos ANSI em logs.
CLIs em pipeline
Uma ferramenta pode receber dados por pipe:
cat users.txt | node cli.jsNesse modo, não mostre prompts que contaminem stdout. Use stderr para mensagens humanas.
Separando stdout e stderr
process.stdout.write(JSON.stringify(result));
process.stderr.write('Processamento concluído\n');Isso permite encadear a saída em outras ferramentas.
Processando linhas
const rl = readline.createInterface({
input,
crlfDelay: Infinity
});
for await (const line of rl) {
await processLine(line);
}A iteração assíncrona respeita o fluxo de processamento e é adequada para arquivos grandes.
Lendo arquivo grande
const fs = require('node:fs');
const file = fs.createReadStream('./data.txt', {
encoding: 'utf8'
});
const rl = readline.createInterface({
input: file,
crlfDelay: Infinity
});
for await (const line of rl) {
if (line.length > 10000) {
throw new Error('Linha muito longa');
}
await processLine(line);
}Consulte File System no Node.js.
crlfDelay
Infinity ajuda a tratar CRLF como uma única quebra mesmo com atraso entre caracteres. É uma opção comum para arquivos multiplataforma.
Linhas vazias
Decida se linha vazia é registro válido, separador ou erro. Não aplique trim automaticamente quando espaços fazem parte do formato.
Backpressure
O loop for await aguarda o processamento antes de pedir a próxima linha. Ainda assim, a stream possui buffers internos. Não acumule todas as linhas em um array.
Concorrência limitada
Se cada linha inicia I/O, uma execução estritamente sequencial pode ser lenta. Use um pool com limite:
const pending = new Set();
const concurrency = 10;
for await (const line of rl) {
const task = processLine(line)
.finally(() => pending.delete(task));
pending.add(task);
if (pending.size >= concurrency) {
await Promise.race(pending);
}
}
await Promise.all(pending);Erros no loop
Quando processLine falha, feche a interface e destrua a stream se necessário. Não continue silenciosamente sem registrar o número da linha.
Contador de linha
let lineNumber = 0;
for await (const line of rl) {
lineNumber += 1;
await processLine(line, lineNumber);
}Histórico
Em terminal interativo, a interface mantém histórico conforme as opções. O histórico pode conter segredos digitados por engano.
historySize
const rl = readline.createInterface({
input,
output,
historySize: 100,
removeHistoryDuplicates: true
});Reduza ou desabilite histórico em comandos sensíveis.
Senhas
question() exibe caracteres digitados. Para senha, use uma biblioteca confiável que desabilite echo ou implemente raw mode com extremo cuidado.
Não registre a resposta e limpe referências assim que possível.
Raw mode
if (input.isTTY) {
input.setRawMode(true);
}Raw mode entrega teclas imediatamente e altera o comportamento de sinais. Sempre restaure em finally:
try {
input.setRawMode(true);
await readSecret();
} finally {
input.setRawMode(false);
}Cursor
O módulo Readline possui APIs para mover cursor e limpar tela. A versão Promises também oferece uma classe de auto commit em versões compatíveis.
ReadlinePromises.ReadStream?
Não confunda a interface Readline com streams do processo. Consulte os nomes exatos na versão utilizada, porque a API evolui.
readlinePromises.ReadLine
A classe de operações de cursor pode enfileirar comandos:
const screen = new readline.Readline(output, {
autoCommit: false
});
screen.clearLine(0);
screen.cursorTo(0);
await screen.commit();Verifique capitalização e disponibilidade na documentação da sua versão.
Atualizando progresso
if (output.isTTY) {
output.write(`Progresso: ${percent}%`);
readline.cursorTo(output, 0);
}Em logs não interativos, imprima linhas normais em intervalos maiores.
Prompt
rl.setPrompt('app> ');
rl.prompt();A interface baseada em Promises ainda pode usar métodos tradicionais para loops interativos.
Loop de comandos
while (true) {
const command = (
await rl.question('app> ')
).trim();
if (command === 'exit') break;
await executeCommand(command);
}Use uma allowlist. Nunca passe a string diretamente para shell.
Segurança de comandos
const commands = {
status: showStatus,
clear: clearCache
};
const handler = commands[command];
if (!handler) {
throw new Error('Comando desconhecido');
}
await handler();Consulte Child Process no Node.js para riscos de shell injection.
Autocomplete
A opção completer fornece sugestões. Ela deve ser rápida e não realizar rede bloqueante a cada tecla.
function completer(line) {
const commands = ['status', 'help', 'exit'];
const hits = commands.filter(item =>
item.startsWith(line)
);
return [hits.length ? hits : commands, line];
}Multilíngue e Unicode
Terminais variam em encoding, largura de caracteres e emojis. Teste em Windows Terminal, Linux, macOS e CI.
Dimensões
const columns = output.columns || 80;
const rows = output.rows || 24;Não presuma que essas propriedades existem em pipes.
Resize
TTY pode emitir evento resize. Recalcule layout sem apagar informação crítica.
Cleanup
async function main() {
const rl = readline.createInterface({ input, output });
try {
await runCli(rl);
} finally {
rl.close();
}
}Process.exit()
Evite chamar process.exit() imediatamente porque stdout pode não terminar de escrever. Defina process.exitCode e feche recursos.
Veja Graceful Shutdown no Node.js.
Testes
Use PassThrough para simular input e capturar output:
const {
PassThrough
} = require('node:stream');
const input = new PassThrough();
const output = new PassThrough();
const rl = readline.createInterface({
input,
output,
terminal: false
});Testando question()
Inicie a Promise e escreva a resposta:
const answerPromise = rl.question('Nome: ');
input.write('Ana\n');
const answer = await answerPromise;Testando timeout
Use timers controlados ou prazo pequeno e confirme que a interface continua sendo fechada.
Testando arquivos
Cubra CRLF, LF, linha final sem quebra, arquivo vazio, linha longa e erro no meio.
Erros comuns
- Não fechar a interface: o processo permanece ativo.
- Mostrar prompt em pipe: saída automatizada fica corrompida.
- Ler senha com question: caracteres ficam visíveis.
- Sem timeout: CI aguarda para sempre.
- Acumular linhas: arquivos grandes consomem memória.
- Comando enviado ao shell: ocorre injection.
- Não restaurar raw mode: terminal fica quebrado.
Boas práticas
- Feche em finally.
- Detecte isTTY.
- Separe stdout e stderr.
- Use AbortSignal.
- Valide respostas.
- Limite tentativas.
- Processe arquivos em stream.
- Controle concorrência.
- Não ecoe senhas.
- Teste terminais e pipes.
Conclusão
O Readline Promises no Node.js simplifica perguntas interativas e processamento linha por linha usando async e await.
A API é mais confiável quando diferencia terminal de pipeline, aplica timeouts, fecha a interface e não acumula arquivos. Com validação, stdout limpo e cuidado com senhas e raw mode, é possível criar CLIs modernas que funcionam tanto para pessoas quanto em automações.



