-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
lib: implement SafeThenable #36326
lib: implement SafeThenable #36326
Conversation
Adds an internal API to handle promises/A+ spec in a consistent way. It uses the built-in Promise.prototype methods when an actual `Promise` instance is given, or lookup and cache the `then` method in the prototype chain otherwise.
Linter failure should be fixed by #36321. |
// The callback is called with nextTick to avoid a follow-up | ||
// rejection from this promise. | ||
process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should run benchmarks on this before landing
#cachedThen; | ||
#hasAlreadyAccessedThen; | ||
|
||
constructor(thenable, makeUnsafeCalls = false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A comment here describing the intent of makeUnsafeCalls
would be good.
Also, I generally prefer avoiding boolean arguments in favor of named flags or an options object. That is, either new SafeThenable(promise, kMakeUnsafeCalls)
where kMakeUnsafeCalls === 1
(allows additional flags to be added later using xor) or new SafeThenable(promise, { unsafeCalls: true })
.
this.#makeUnsafeCalls = makeUnsafeCalls; | ||
} | ||
|
||
get #then() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I'm fine with using this syntax in general, we should benchmark this extensively before landing. Last I checked, v8 was not yet optimizing private accessors that well and since we're using this in EventEmitter (one of the most performance sensitive bits of code we have in core) it's good to be careful here.
#makeUnsafeCalls; | ||
#target; | ||
#cachedThen; | ||
#hasAlreadyAccessedThen; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are still a number of performance concerns around using private fields (unfortunately). These should likely be non-exported Symbols instead.
result, | ||
if (!SafeThenable) | ||
SafeThenable = require('internal/per_context/safethenable'); | ||
new SafeThenable(result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should wrap this in a lazySafeThenable()
function to avoid the code duplication
this.then(undefined, onError); | ||
} | ||
|
||
then(...args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is terrifying, even if we ignore things like "the returned thenable is missing finally" or "if you do `.constructor you don't get all the promise statics. This is still terrifying ^^
I am -0.5, if others feel strongly then maybe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intended to be used with objects for which only .then
method is called. It's just a wrapper for the pattern that shows several times in core:
const then = obj?.then;
if(typeof then === function) {
FunctionPrototypeCall(then, obj, onSuccess, onError);
}
I think .finally
is a bit off-topic, we should always prefer PromisePrototypeFinally
anyway.
} | ||
|
||
get #then() { | ||
// Handle Promises/A+ spec, `then` could be a getter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we care about the Promises/A+ spec in the first place? The only spec that we care about is the JavaScript spec - if we got a native promise this should never happen right?
The "double getteR" thing is an issue (thanks jQuery!) with assimilation mostly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is not intended to be used with genuine Promise
object, it's just for API that have to deal with user-provided thenable objects (E.G.: util.callbackify
).
} | ||
if (!SafeThenable) | ||
SafeThenable = require('internal/per_context/safethenable'); | ||
new SafeThenable(promise).catch(function(err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What advantage does this have over { await thenable } catch (err) { ...
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not awaiting the thenable at all – it attaches a catch handler in case the event handler returns a thenable. I'm pretty sure that's in the spec, although I haven't checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok sorry, I hate blocking things but I'm starting to feel strongly there are footguns here.
I'd prefer an approach refactoring unguarded then
s we are concerned about to await
s with native async await which is always safe anyway.
@benjamingr What footguns are thinking about? Do you think we could avoid that by changing the name of the class and its method?
You mean something like: const createSafeThenable = (target) => new Promise(async (_) => _(await target)); The issue I can think of with this code is that it creates another |
That can just be |
If the target The issue with @benjamingr could you please express what are the footguns you think the current implementation could create? I'm willing to try to fix those if that's possible. |
FWIW: I raised this issue in Node (the double get thing from the A+ spec) once as a rhetorical tool to illustrate there are many edge cases in the spec. Since then I keep seeing this as an issue here but it's really not a big deal in real life and I wouldn't worry about it. There was one version of jQuery like... 10 years ago that had a bug and assimilating promises caused issues. I am not aware of any real
That's fair, the common pattern is indeed You can extract that check into a helper if you think it repeats a lot I guess? function callThenIfPromise(maybeThenable, onFulfilled) {
const then = maybeThenable?.then;
if(typeof then === 'function')
FunctionPrototypeCall(then, maybeThenable, onFulfilled); // Or PromisePrototypeThen if you prefer
} Which is much simpler (right?). The code after this PR isn't actually shorter or clearer from what I can tell.
Sure, always happy to talk about promise footguns, none of these are huge but they were pretty quick to spot:
If the goal here is to make code safer I think it's better to call the If you want to make sure |
Fair points, thanks for the explanation. |
Adds an internal API to handle promises/A+ spec in a consistent way. It
uses the built-in Promise.prototype methods when an actual
Promise
instance is given, or lookup and cache the
then
method in theprototype chain otherwise.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes