Skip to content
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

Unexpected await and Promise behavior #31392

Closed
jaydenseric opened this issue Jan 17, 2020 · 13 comments
Closed

Unexpected await and Promise behavior #31392

jaydenseric opened this issue Jan 17, 2020 · 13 comments
Labels
assert Issues and PRs related to the assert subsystem.

Comments

@jaydenseric
Copy link
Contributor

jaydenseric commented Jan 17, 2020

  • Version: v10.18.1, v13.5.0
  • Platform: macOS v10.15.2 (19.2.0 Darwin Kernel Version 19.2.0: Sat Nov 9 03:47:04 PST 2019; root:xnu-6153.61.1~20/RELEASE_X86_64 x86_64)
  • Subsystem: ?

Using await on a Promise that never resolves or rejects behaves very strangely. I would expect it to simply hang, indefinitely awaiting the promise.

What actually happens is that the rest of the script outside the async function carries on normally, and the process exits fast and without an error.

// demo.js

const promise = new Promise(() => {})

async function demo1() {
  await promise
  console.log('This never logs.')
}

async function demo2() {
  await require('assert').rejects(promise)
}

demo1()
demo2()
console.log('This logs.')

Result:

Screen Shot 2020-01-17 at 12 03 07 pm

It's concerning that in the case of demo 2, a test could in a way be skipped at the point of a rejects assertion, without any exception or warning even though the promise never actually rejected.

I would prefer await on a promise that never resolves or rejects hangs indefinitely, that way when testing locally you can tell something is wrong, and CI can timeout and exit with an error.

Tweet thread: https://twitter.com/jaydenseric/status/1217964277727809536

@devsnek
Copy link
Member

devsnek commented Jan 17, 2020

Duplicate of nodejs/promises-debugging#16

@devsnek devsnek marked this as a duplicate of nodejs/promises-debugging#16 Jan 17, 2020
@devsnek devsnek closed this as completed Jan 17, 2020
@devsnek devsnek reopened this Jan 17, 2020
@devsnek
Copy link
Member

devsnek commented Jan 17, 2020

@nodejs/assert should assert.resolves and assert.rejects register a check for process exit?

@devsnek devsnek marked this as not a duplicate of nodejs/promises-debugging#16 Jan 17, 2020
@devsnek devsnek added the assert Issues and PRs related to the assert subsystem. label Jan 17, 2020
@ljharb
Copy link
Member

ljharb commented Jan 17, 2020

What do you mean by “a check”? Like, if they haven’t settled by the time the process exits, they throw?

@devsnek
Copy link
Member

devsnek commented Jan 17, 2020

@ljharb yes

@jaydenseric
Copy link
Contributor Author

jaydenseric commented Jan 17, 2020

For other people trying to work around this issue, here is a clever way to assert if a promise is resolved or rejected, that won't silently let you down if it's never either:

await assert.rejects(Promise.race([
  // Promise instance to assert.
  promise,

  // Thoeretically you could use any value except Promise.reject() here.
  Promise.resolve()
]))

This is only a solution if you want to assert the status at an instant in time; it won't wait around to check how the promise eventually settles.

Maybe this could pattern could be available as a new assert.rejected API:

await assert.rejected(promise)

The past-tense naming makes it obvious the assertion won't wait around for a promise to settle.

@himself65
Copy link
Member

relacted: #29355

@Fishrock123
Copy link
Contributor

@nodejs/assert should assert.resolves and assert.rejects register a check for process exit

I think they probably should.

@DavenportEmma
Copy link
Contributor

Could I take this issue? Try to implement the warning about unresolved promises mentioned here

@jasnell
Copy link
Member

jasnell commented Feb 17, 2020

@conordavenport ... this could be a good one, yes, but it could be tricky.

In most cases when Node.js exits normally, the process.on('exit') event is emitted. This gives us an opportunity to perform some last minute checks, cleanups, etc. The code running within the exit handler must be fully synchronous as the Node.js process exits as soon as the callbacks are done. process.nextTick(), Promises, etc will not be processed.

The idea here would be for assert.rejected() or assert.resolves() to keep tabs of any Promises passed to it to see if those have settled by the time the process exits. That means we have to keep a record of those and keep them up to date as their status changes -- which would mean keeping a map of weak references to the Promises such that (a) Promises that settle are removed automatically and (b) we can quickly and efficiently iterate through unsettled Promises and error on those. Failure to do this record keeping correctly can lead to significant memory leaks so it must be done carefully.

This also raises a question: What if there are multiple unsettled promises at Promise exit? Do we just throw a single Assertion error? Do we print multiple warnings? etc. Before doing the implementation on this time should be taken on what the expected experience should be.

@jasnell
Copy link
Member

jasnell commented Feb 17, 2020

Also note: it would be fairly trivial for user code to implement a rudimentary Promise status tracker using the async_hooks API.

@addaleax
Copy link
Member

This also raises a question: What if there are multiple unsettled promises at Promise exit? Do we just throw a single Assertion error? Do we print multiple warnings? etc. Before doing the implementation on this time should be taken on what the expected experience should be.

I wouldn’t throw anything from a .on('exit') handler. I think printing warnings for each failed assertion would be fine, together with setting process.exitCode to a non-zero value (if it does not already have a non-zero value)?

@jasnell
Copy link
Member

jasnell commented Feb 17, 2020

Thinking about this further, porting the common.mustCall() and common.mustNotCall() from the Node.js test suite to the assert module might actually make this significantly easier here. It particularly would avoid any need to do explicit Promise state tracking. Specifically, to know if a given Promise has settled by process exit, it would be:

doAsyncThing().finally(assert.mustCall())

@addaleax's warning about throwing in process exit is valid, however... so rather than throwing an assertion error on process.on('exit') it should print a warning... just keep in mind, however, that process.emitWarning() emits on process.nextTick() so that can't be used directly.

@bnoordhuis
Copy link
Member

Failure to do this record keeping correctly can lead to significant memory leaks so it must be done carefully.

Doing this record keeping correctly also leads to massive memory leaks when there are many unresolved promises, even more so if it also retains call stacks.

IMO, a better approach is to invest effort in improving the debugger experience for promises. For example, showing the 25 longest-living promises in the debugger will probably go a long way towards tracking down bugs, and that's something that can be implemented with a simple generation counter.

DavenportEmma added a commit to DavenportEmma/node that referenced this issue Apr 10, 2020
BethGriggs pushed a commit that referenced this issue Apr 27, 2020
Fixes: #31392

PR-URL: #31982
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Zeyu Yang <himself65@outlook.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
BridgeAR pushed a commit that referenced this issue Apr 28, 2020
Fixes: #31392

PR-URL: #31982
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Zeyu Yang <himself65@outlook.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
targos pushed a commit that referenced this issue May 4, 2020
Fixes: #31392

PR-URL: #31982
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Zeyu Yang <himself65@outlook.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
MylesBorins pushed a commit that referenced this issue Aug 17, 2020
Fixes: #31392

PR-URL: #31982
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Zeyu Yang <himself65@outlook.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assert Issues and PRs related to the assert subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants