nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
105.62k stars 28.65k forks source link

skipFiles does not suppress "uncaught" exceptions in web streams #53789

Open rotu opened 2 weeks ago

rotu commented 2 weeks ago

Version

22.4.1

Platform

Microsoft Windows NT 10.0.22635.0 x64

Subsystem

No response

What steps will reproduce the bug?

Run this code in the VSCode debugger with "skipFiles":["<node_internals>/**"] in the launch configuration.

import net from 'node:net';
import stream from 'node:stream';
const socket = net.connect('http://host.invalid.');
socket.on('error', (e) => {
   console.error('Caught the error', e.message);
});
let myStream = stream.Duplex.toWeb(socket);

setTimeout(() => {
   console.log('done');
}, 1000);

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

No response

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

It is expected that the debugger won't trip. This promise rejection is entirely created and handled within node's internals and should be suppressed by the VSCode's skipFiles filter.

What do you see instead?

The debugger breaks when the promise is rejected.

The trace (given by console.trace) when it is rejected looks like:

Trace
    at eval (eval-7c2224bb.repl:1:9)
    at Object.reject (<anonymous>)
    at writableStreamRejectCloseAndClosedPromiseIfNeeded (node:internal/webstreams/writablestream:771:28)
    at writableStreamFinishErroring (node:internal/webstreams/writablestream:903:5)
    at writableStreamStartErroring (node:internal/webstreams/writablestream:755:5)
    at writableStreamDefaultControllerError (node:internal/webstreams/writablestream:1202:3)
    at WritableStreamDefaultController.error (node:internal/webstreams/writablestream:502:5)
    at Socket.<anonymous> (node:internal/webstreams/adapters:195:18)
    at Socket.<anonymous> (node:internal/util:538:20)
    at Socket.onclose (node:internal/streams/end-of-stream:152:25)

The stack on the error object is:

'AbortError: The operation was aborted
    at handleKnownInternalErrors (node:internal/webstreams/adapters:111:14)
    at Socket.<anonymous> (node:internal/webstreams/adapters:179:13)
    at Socket.<anonymous> (node:internal/util:538:20)
    at Socket.onclose (node:internal/streams/end-of-stream:152:25)
    at Socket.emit (node:events:532:35)
    at Pipe.<anonymous> (node:net:339:12)
    at Pipe.callbackTrampoline (node:internal/async_hooks:130:17)'

Screenshot 2024-07-09 133433

Additional information

No response

rotu commented 2 weeks ago

Related: #51093 (the debugger falsely believes promise rejections in web streams are unhandled).

RedYetiDev commented 2 weeks ago

I believe skipFiles is controlled by VSCode, not Node.js, though I could be mistaken.

Issue #51093 reports to a problem in the V8 Debugger, whereas this one is about an issue in VSCode. I was thinking of #53732, whoops.

rotu commented 2 weeks ago

Yes, skipFiles is controlled by VSCode. And the Chrome debugger supports a similar "ignore list" feature. Adding the pattern ^node:internal does not seem to suppress the debugger. Even adding the pattern .* does not seem to suppress the debugger from breaking here!

If there's a more correct way to suppress these exceptions, please let me know!

RedYetiDev commented 2 weeks ago

It sounds to me like this is a VSCode bug, and not a Node.js one, but again, I may be mistaken.

rotu commented 2 weeks ago

I'm not sure what the pattern is supposedly matching against when the debugger is deciding whether to pause at a breakpoint - the stack traces in play (shown above) seem to match ignore-listed scripts, so I'd expect there not to be a breakpoint hit.

It sounds to me like this is a VSCode bug, and not a Node.js one, but again, I may be mistaken.

Might be, but if so, then Chrome has the same behavior. I can't tell if there's any workaround to #53732 that allows trapping uncaught exceptions without making async code to "nuisance trip" the debugger on an eventually-handled rejection.

RedYetiDev commented 2 weeks ago

@nodejs/v8 - is this a V8 debugger issue, or a VSCode/Chrome issue?

benjamingr commented 2 weeks ago

The promise is rejected at writableStreamRejectCloseAndClosedPromiseIfNeeded and is then immediately handled to prevent it from having an unhandled rejection which is why Node.js doesn't report an unhandled rejection here:

  stream[kIsClosedPromise].reject(stream[kState]?.storedError);
  setPromiseHandled(stream[kIsClosedPromise].promise);

The other instances I can trigger this are similar:

  stream[kIsClosedPromise].reject(error);
  setPromiseHandled(stream[kIsClosedPromise].promise);

This indeed seems overly eager (and incorrect) on V8's side. Additionally skipFiles not working seems like a VSCode bug.

I'll open a PR with a workaround from our side until it's addressed there. Those aren't real errors.

benjamingr commented 2 weeks ago

https://github.com/nodejs/node/pull/53800

RedYetiDev commented 2 weeks ago

Given the discussion on the linked PR, it seems like this isn't actionable from Node.js's side. I suggest opening an issue with VSCode / V8.

Feel free to close the issue if you agree

RedYetiDev commented 1 week ago

Reverting close state to author's close

benjamingr commented 1 week ago

This isn't closed, the fact skipFiles does not suppress exceptions in core and that it's still a caught exception is a bug and we should track it since it affects our users even if we can't fix it.

RedYetiDev commented 1 week ago

we should track it since it affects our users even if we can't fix it

Is this being tracked anywhere that it is actionable? From what I can tell, V8's trackers show related issues as (mostly) fixed, although it appears otherwise.

jasnell commented 1 week ago

FWIW, there's a v8 internal API that is likely relevant here...v8::Promise::MarkAsSilent ... https://v8docs.nodesource.com/node-22.4/d3/d8f/classv8_1_1_promise.html#a085c428f3f9600830a87819f99e9fda6 ... there is likely a performance impact but might be possible for us to expose this API to JavaScript internally.

rotu commented 1 week ago

@jasnell Interesting. I'm not sure that's going to help in this case. At least for the web streams API, these rejected promises should trip the debugger if the user awaits them (but V8 doesn't do this - it only trips the debugger when the promise becomes rejected, if at all).

jasnell commented 1 week ago

Given the way the promises work, if I await a promise it creates a new promise... So the pattern here is:

Create P1 ... Mark P1 as handled and silent. If P1 rejects then debugger will not be triggered.

Await P1... Creates P2

When P1 rejects, P2 is rejected. P2 is not marked as handled or silent, so P2 will trigger the debugger. I think this is exactly the pattern that is needed here.

rotu commented 1 week ago

@jasnell I wish it worked that way, and I believe that is the way it works for unhandledrejection events.

The debugger, on the other hand, seems to trip based on the explicit throw or Promise.reject call from user code.

All the below examples create unhandled rejections. IMO they should all pause the debugger:

const x = Promise.reject(new Error("on purpose"))

// these trip the debugger:
const a1 = x.catch((e) => { throw e })
const a2 = x.catch((e) => { Promise.reject(e) })

// none of these trip the debugger:
const b1 = x.then()
const b2 = Promise.resolve(x)
const b3 = new Promise((resolve) => { resolve(x) })
const b4 = Promise.resolve().then(() => x)
const b5 = Promise.resolve({ then: (resolve) => resolve(x) })
const b6 = (async () => { await x })()

// none of these trip the debugger either:
const b7 = new Promise((resolve, reject) => { x.then(resolve, reject) })
const b8 = x.catch(
  (e) => ({
    then: (resolve, reject) => { reject(e) }
  })
)
const b9 = x.catch(Promise.reject.bind(Promise))

edit: even worse the above cases only report the stack trace where the Error object was created (at least with node flags set to --unhandled-rejections=warn --trace-warnings --trace-uncaught --report-uncaught-exception --async-stack-traces) and the stack trace to emitUnhandledRejectionWarning. From the output you can't tell which line is responsible for which warning! Even --trace-promises doesn't help much.