Skip to content

Unhandled Exception in readline interface #58289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
BenderMarkusKremer opened this issue May 12, 2025 · 3 comments
Open

Unhandled Exception in readline interface #58289

BenderMarkusKremer opened this issue May 12, 2025 · 3 comments

Comments

@BenderMarkusKremer
Copy link

BenderMarkusKremer commented May 12, 2025

Version

24.0.1

Platform

Darwin ... 24.4.0 Darwin Kernel Version 24.4.0: Fri Apr 11 18:33:47 PDT 2025; root:xnu-11417.101.15~117/RELEASE_ARM64_T6030 arm64

Linux ... 6.8.0-39-generic #39-Ubuntu SMP PREEMPT_DYNAMIC Sat Jul  6 02:50:39 UTC 2024 aarch64 GNU/Linux

Subsystem

readline

What steps will reproduce the bug?

The following code creates a dummy fs that throws an error on the fs.close() call.
This error can be caught with events.once() when using the fs directly.
When using the fs together with readline, the error is not caught by events.once().

import { once } from 'events'
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

let position = 0
const myDummyFilesystem = {
  read: (fd, buffer, offset, len, pos, cb) => { buffer.write("A\n"); cb(null, ((position += 2) <= 10) ? 2 : 0, buffer) },
  close: (x, cb) => { cb(new Error("close failed")) },
  open: (name, flags, mode, cb) => { cb(undefined, 42) }
}

async function bad() {
  const file = createReadStream('app.js', { fs: myDummyFilesystem })
  const rl = createInterface({ input: file })
  let lineNr = 0
  rl.on('line', (line) => console.log(`bad ${++lineNr} ${line}`))
  await once(rl, 'close') // DOES NOT CATCH Error of the fs.close() CALL
}

async function good() {
  const file = createReadStream('app.js', { fs: myDummyFilesystem })
  let lineNr = 0
  file.on('data', (line) => console.log(`good ..`))
  await once(file, 'close') // CATCHES Error of the fs.close() CALL
}

await (good().catch(e => console.error("nice first catch", e)))
position = 0
bad().catch(e => console.error("nice second catch", e))

How often does it reproduce? Is there a required condition?

always

What is the expected behavior? Why is that the expected behavior?

the await once(...) also catches the error on fs.close()

What do you see instead?

Unhandled Exception error

Additional information

No response

@aduh95
Copy link
Contributor

aduh95 commented May 12, 2025

It'd be useful that you paste a error stack trace.

You haven't answered the "Why is that the expected behavior?" question, IMO if the read stream fails to close is different from the readline failing to close, you should listed to the 'error' and/or 'close' even of the read stream.

@juanarbol
Copy link
Member

juanarbol commented May 12, 2025

Hi, this is not a bug per-se; you should be listening to the stream errors, not the readline errors. The readline is not failing, it is your stream.

import { once } from 'events'
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

let position = 0
const myDummyFilesystem = {
  read: (fd, buffer, offset, len, pos, cb) => { buffer.write("A\n"); cb(null, ((position += 2) <= 10) ? 2 : 0, buffer) },
  close: (x, cb) => { cb(new Error("close failed")) },
  open: (name, flags, mode, cb) => { cb(undefined, 42) }
}

async function bad() {
  const file = createReadStream('app.js', { fs: myDummyFilesystem })
  const rl = createInterface({ input: file })
  let lineNr = 0
  rl.on('line', (line) => console.log(`bad ${++lineNr} ${line}`))
  await once(file, 'close') // NOW DOES CATCH Error of the fs.close() CALL
}

async function good() {
  const file = createReadStream('app.js', { fs: myDummyFilesystem })
  let lineNr = 0
  file.on('data', (line) => console.log(`good ..`))
  await once(file, 'close') // CATCHES Error of the fs.close() CALL
}

await (good().catch(e => console.error("nice first catch", e)))
position = 0
bad().catch(e => console.error("nice second catch", e))

You could consider that snippet as a solution for this. But this may raise some discussion around what events we listen to from the backing stream in readline.

cc // @nodejs/streams

@BenderMarkusKremer
Copy link
Author

If i fail in the the fs.read() call instead of the fs.close() call, the error is caught by the readline interface with await once(rl, 'close') .

I would expect that i either need to handle all input stream errors on my own, or that the readline interface provides an wrapper that abstracts/forwards all errors of the input stream.

Output with read errors:

node nodeError2.js
nice first catch v23.11.0 Error: read failed
    at file:///Users/js/readline/nodeError2.js:8:17
    at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)
.
.
.
.
.
node:events:485
      throw er; // Unhandled 'error' event
      ^

Error: close failed
    at file:///Users/js/readline/nodeError2.js:27:14
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
Emitted 'error' event on ReadStream instance at:
    at emitErrorNT (node:internal/streams/destroy:170:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21)

new Code with read error:

import { once } from 'events'
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

let readError = new Error("read failed")
let closeError = null
let position = 0
const myDummyFilesystem = {
  read: (fd, buffer, offset, len, pos, cb) => { buffer.write("A\n"); cb(readError, ((position += 2) <= 10) ? 2 : 0, buffer) },
  close: (x, cb) => { cb(closeError) },
  open: (name, flags, mode, cb) => { cb(undefined, 42) }
}

async function fun() {
  const file = createReadStream('app.js', { fs: myDummyFilesystem })
  const rl = createInterface({ input: file })
  let lineNr = 0
  rl.on('line', (line) => console.log(`.`))
  await once(rl, 'close') // DOES NOT CATCH Error of the fs.close() CALL
}

await fun().catch(e => console.error("nice first catch", process.version, e))
readError = null
closeError = new Error("close failed")
position = 0
await fun().catch(e => console.error("nice second catch", process.version, e))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants