rl.on('line')

Registers a listener called with each complete line the readline interface reads from its input.

Signature
rl.on('line', listener)
  • Added in Node.js v0.1.98

Parameters

ParameterTypeDescription
$'line'stringThe event name. Emitted whenever the input stream receives an end-of-line marker (\n, \r or \r\n).
$listenerFunctionCalled as listener(input), where input is the line as a string with the line terminator removed.

Return value

readline.InterfaceThe interface itself, so listener registrations can be chained.

Errors

Throws ERR_INVALID_ARG_TYPE if listener is not a function. Errors thrown inside the listener are not caught by readline and become uncaught exceptions.

The 'line' event is the original, and still the fastest, way to consume a readline interface. It fires once per complete line, with the terminator stripped, and once more at the end of the stream if the last line has no trailing newline. On a TTY it fires when the user presses Enter.

When to reach for it

  • Scanning a very large file where throughput matters — the event API is measurably faster than for await.
  • Command loops on process.stdin, where you also want rl.prompt() after each line.
  • Anywhere you need several listeners on the same interface.

What trips people up

The listener is synchronous from readline's point of view. If it returns a promise, readline does not wait: lines keep arriving and your async work fans out unbounded. Either keep the handler synchronous, or call rl.pause() before the async work and rl.resume() after it — or switch to for await, which applies backpressure for free.

Lines can still be delivered after rl.close(), because data already buffered in the stream is drained. Guard with a flag if that matters.

Errors on the input stream are not routed through the interface. Attach 'error' to the input stream yourself.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Count HTTP status codes in a large access log without buffering it
  • Drive an interactive command prompt where each Enter runs a command
  • Filter a text stream and write matching lines to a second file
  • Tail a fixture file in a test and assert on each parsed record

Frequently asked questions

Why does my async 'line' handler run out of order?

readline does not await the listener, so it emits the next line immediately and your promises overlap. Call rl.pause() at the top of the handler and rl.resume() when the async work finishes, or replace the event with a for await...of loop over the interface.

Does the 'line' event include the newline character?

No. The terminator (\n, \r or \r\n) is stripped before the listener is called, so a blank line arrives as an empty string. A final line without a trailing newline is still emitted when the stream ends.

Why do 'line' events still fire after I called rl.close()?

close() relinquishes the streams but does not discard data readline has already buffered, so queued lines are delivered afterwards. Set a flag in your 'close' handler and ignore lines that arrive once it is set.

More examples

Pausing for async work per line

pause() stops the input stream flowing while saveUser() is in flight, so at most one record is written at a time. Without the pause/resume pair every line in the file would call saveUser() before the first promise settled.

Example · javascript
import fs from 'node:fs';
import readline from 'node:readline';
import { once } from 'node:events';

const rl = readline.createInterface({
  input: fs.createReadStream('users.ndjson'),
  crlfDelay: Infinity,
});

rl.on('line', (line) => {
  rl.pause();
  saveUser(JSON.parse(line))
    .then(() => rl.resume())
    .catch((err) => {
      console.error('row failed:', err.message);
      rl.close();
    });
});

await once(rl, 'close');
console.log('done');

A command loop with prompt redraw

Each Enter emits one 'line' with the terminator removed, so line.trim().split(' ') sees exactly what was typed. Calling rl.prompt() at the end of the handler redraws '> ' for the next command; 'close' runs on quit or Ctrl+D.

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

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

const todo = [];
rl.prompt();

rl.on('line', (line) => {
  const [cmd, ...rest] = line.trim().split(' ');
  if (cmd === 'add') todo.push(rest.join(' '));
  else if (cmd === 'list') todo.forEach((t, i) => console.log(`${i + 1}. ${t}`));
  else if (cmd === 'quit') return rl.close();
  else console.log('add <text> | list | quit');
  rl.prompt();
});

rl.on('close', () => console.log(`${todo.length} item(s) left`));

Handling stream errors alongside lines

A missing file emits ENOENT on the read stream, not on the interface. Without the input.on('error') handler the process would crash with an unhandled 'error' event; with it, 'close' still fires and reports zero lines.

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

const input = fs.createReadStream('maybe-missing.txt');

input.on('error', (err) => {
  console.error('cannot read input:', err.code);
  process.exitCode = 1;
});

const rl = readline.createInterface({ input, crlfDelay: Infinity });

let n = 0;
rl.on('line', () => n++);
rl.on('close', () => console.log('lines seen:', n));

Discussion

  • Be the first to comment on this lesson.