denoland/deno · error · ERR_USE_AFTER_CLOSE

ERR_USE_AFTER_CLOSE

ERR_USE_AFTER_CLOSE

Error message

readline was closed

What it means

The internal kQuestion path backs rl.question(); once rl.close() has run (closed = true, 'close' emitted), calling question throws ERR_USE_AFTER_CLOSE('readline'). The interface cannot queue a question callback on a closed input stream, so this is a hard programmer error, not a race.

Source

Thrown at ext/node/polyfills/internal/readline/interface.mjs:437

  /**
   * Writes the configured `prompt` to a new line in `output`.
   * @param {boolean} [preserveCursor]
   * @returns {void}
   */
  prompt(preserveCursor) {
    if (this.paused) this.resume();
    if (this.terminal && op_get_env_no_permission_check("TERM") !== "dumb") {
      if (!preserveCursor) this.cursor = 0;
      this[kRefreshLine]();
    } else {
      this[kWriteToOutput](this[kPrompt]);
    }
  }

  [kQuestion](query, cb) {
    if (this.closed) {
      throw new ERR_USE_AFTER_CLOSE("readline");
    }
    if (this[kQuestionCallback]) {
      this.prompt();
    } else {
      this[kOldPrompt] = this[kPrompt];
      this.setPrompt(query);
      this[kQuestionCallback] = cb;
      this.prompt();
    }
  }

  [kOnLine](line) {
    if (this[kQuestionCallback]) {
      const cb = this[kQuestionCallback];
      this[kQuestionCallback] = null;
      this.setPrompt(this[kOldPrompt]);
      cb(line);
    } else {

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Check rl.closed before asking: if (rl.closed) return; (or recreate the interface)
  2. Wait for the current question's callback/answer before closing on signals
  3. Create a fresh createInterface per prompt session instead of reusing a closed one
  4. Use readline/promises question() and await it before any close path runs

Example fix

// before
rl.on('SIGINT', () => rl.close());
setTimeout(() => rl.question('again? ', cb), 1000); // may fire after close

// after
rl.on('SIGINT', () => rl.close());
setTimeout(() => { if (!rl.closed) rl.question('again? ', cb); }, 1000);
Defensive patterns

Strategy: validation

Validate before calling

function ask(rl: readline.Interface, q: string): Promise<string> | null {
  if (rl.closed) return null; // caller should recreate the interface
  return new Promise((res) => rl.question(q, res));
}

Type guard

const isOpen = (rl: { closed: boolean }): boolean => !rl.closed;

Try / catch

try {
  rl.question(q, cb);
} catch (err) {
  if (err?.code === 'ERR_USE_AFTER_CLOSE') {
    rl = readline.createInterface({ input, output });
    rl.question(q, cb);
  } else throw err;
}

Prevention

When it happens

Trigger: rl.question('Name?', cb) after rl.close(); asking inside an event handler that fires after close; a REPL loop that closes the interface on 'exit'/'SIGINT' but a pending setTimeout or promise resolution then calls question again.

Common situations: Prompt queues that keep asking questions while an abort/SIGINT handler already closed the rl; sequential async ask() helpers that race user Ctrl+C against the next question; reusing one interface across sessions instead of recreating it.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/79a6b8ec09c1c581. Report an issue: GitHub.