Alternative names for `withResolvers()`
shgysk8zer0 opened this issue · comments
I am sure I'm not the only one who thinks Promise.withResolvers()
does not sound right. To see the name, I would expect it to be something that you pass one or more resolve
s into and it returns a Promise
.
I think getResolvers()
makes more sense, but it's still not descriptive of what it does. It returns the promise
as well.
I don't have a good name, but "extract", "expose", "unwrap", & "destructure" seem like more descriptive words that imply what it would do. What exactly would you call { promise, resolve, reject }
? What would you call the object that it returns?
That's called a "deferred", colloquially, and in jQuery.
defer
is a reasonable name, but es6-shim aggressively deletes Promise.defer
since Chrome shipped it (despite it not being in the spec) for many years.
I've always called these "deferreds", so I'd be happy with Promise.deferred()
I've always called these "deferreds", so I'd be happy with
Promise.deferred()
That makes sense, but I assumed the name withResolvers()
was attempting to distance Promises from deferreds. It's the name I already use.
const { promise, resolve, reject } = Promise.resolvers()
this mirrors Object.entries()
, and doesn't risk any possible interpretational overload of the already existing with
keyword.
Promises are a subset of Deferreds, so i'm not sure distance is required.
My reasoning for avoiding defer
/deferred
is that it is not a useful label for people who are not already familiar with that specific name. History aside it doesn't really make sense as a label. My understanding is that the name comes from Python Deferreds, where Deferred is Python's name for what are in EcmaScript called Promises. (And since promises are devices for deferred execution, I think that it comes across that way to people who are not familiar with Python Deferreds per se.) So a method called Promise.defer
is just a synonym for Promise.promise
or Promise.executeLater
or something. I don't think any of those would be good labels.
Whatever we call it, users who already know this method as defer
or deferred
should have no problem understanding what the method does. I'd instead prefer a label that gives new developers or developers who do not frequently encounter the method a clear idea of what it does. Admittedly this is a bit hard; the idea is that Promise.withResolvers
gives you a promise, along with its resolvers, so that's why I chose it to start with. But I welcome alternative proposals!
My issue with getResolvers
is it sounds like you're getting just the resolvers for some reason rather than the promise together with the resolvers.
Promise.evert()
- "to turn inside out", though the visual closeness of "evert" to "event" is undesirablePromise.create()
likeObject.create()
Promise.parts()
similar to but not.entries()
- I'd suggest
Promise.terminals()
but{resolve, reject, promise}
doesn't just have ends, instead 2 categories of things atm
Why it has to be a static method? Why not change the executor to be optional? Shouldn't this much easier to use?
const promise = new Promise;
promise.resolve(1);
console.log(await promise);
// polyfill
const LegacyPromise = globalThis.Promise;
globalThis.Promise = class Promise extends LegacyPromise {
constructor(executor) {
super (
executor ?? (resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
}
);
}
}
@fisker that would both mask and create bugs; it would be a very bad idea to accidentally expose a promise's resolvers.
My understanding is that the name comes from Python Deferreds, where Deferred is Python's name for what are in EcmaScript called Promises. (And since promises are devices for deferred execution, I think that it comes across that way to people who are not familiar with Python Deferreds per se.) So a method called
Promise.defer
is just a synonym forPromise.promise
orPromise.executeLater
or something. I don't think any of those would be good labels.
I'm assuming you means twisted's deferred here, in which case it's the exact same thing as we're proposing here. It's not a promise, but a promise and its resolve/reject (called callback
and errback
), and the .then
chaining methods, all on the same object.
Whatever we call it, users who already know this method as
defer
ordeferred
should have no problem understanding what the method does. I'd instead prefer a label that gives new developers or developers who do not frequently encounter the method a clear idea of what it does.
I think this isn't a great argument to break with a naming precedent. What is a "promise"? Well developers didn't know what it was until they learned what it was. And what's a "deferred"? They'll learn it when they learn it, unless they already know it because it's a popular name for an existing pattern.
it would be a very bad idea to accidentally expose a promise's resolvers.
Promise.withResolvers() // Object {promise, resolve, reject}
new Promise // Promise {resolve, reject}
What's the difference?
@fisker new Promise()
is a very easy mistake to make when intending new Promise((resolve) => resolve())
or Promise.resolve()
- nobody's going to type Promise.withResolvers()
by accident.
How do you think?
const promise = Promise.withResolvers();
promise.resolve(1);
That's a real "with resolvers", since resolvers are assigned to promise. 😄
@fisker i'm not sure what you're arguing. Specifically, when typing new Promise()
, omitting the executor is a mistake, and it is bad language design to turn a mistake into something that quietly does something different.
How about accept an option?
const promise = new Promise({
defer: true,
// Or
exposeResolvers: true,
});
// Or
const promise = Promise.create({exposeResolvers: true})
promise.resolve(1);
console.log(await promise);
That would certainly be a viable alternative.
with
has been deprecated for ages. I'm not really concerned with overload there. If there is a better term, fine, but I'm not convinced we need to deliberately avoid with
for that reason.
How about accept an option?
const promise = new Promise({ defer: true, // Or exposeResolvers: true, }); // Or const promise = Promise.create({exposeResolvers: true}) promise.resolve(1); console.log(await promise);
first option is a non-starter because the Promise
constructor takes an executor
as its only argument.
2nd option has cognitive friction (for me anyway) due to Object.create(proto, propertiesObject)
more of an aside, but naming the wrapper object variable promise
is confusing due to overload with static Promise.resolve()
and leading to promise.promise.then()
per #1 it seems more and more like we are necromancing Promise.defer()
and should consider leaning into that
I'm assuming you means twisted's deferred here, in which case it's the exact same thing as we're proposing here. It's not a promise, but a promise and its resolve/reject (called
callback
anderrback
), and the.then
chaining methods, all on the same object.
The current proposal has it that there's no .then
method on the thing returned by Promise.withResolvers
. It's just a POJO with the promise, resolve and reject. Here:
interface PromisePlusItsResolvers<T> {
promise: Promise<T>
resolve: (value: T) => void
reject: (reason?: string) => void
}
class Promise {
static withResolvers<T>(): PromisePlusItsResolvers<T>;
}
I think this isn't a great argument to break with a naming precedent.
It's not much of a precedent -- there's no precedent in the spec, nor is it something widespread in other PLs. It exists in the ecosystem but even there Deferreds are not exactly the same thing as what Promise.withResolvers
returns.
What is a "promise"? Well developers didn't know what it was until they learned what it was. And what's a "deferred"? They'll learn it when they learn it, unless they already know it because it's a popular name for an existing pattern.
Promises were a new primitive that needed a concise name, and "promise" is highly evocative of what they do. Agreed that developers were never going to be able to intuit what they were from just their name and you're right that that's okay in that case. But that's not the situation here. This is just a POJO that contains a promise along with its resolve and reject functions. We don't really need a concise name for that thing because 99 times out of 100 people are going to just destructure it anyway.
The spec calls these "capabilities", which suggests Promise.capability()
, which... I don't hate, I guess. It's shorter than withResolvers
, at least.
The current proposal has it that there's no
.then
method on the thing returned byPromise.withResolvers
.
Yes, they are slightly different because Twisted doesn't have a promise object and we store the the .then
on promises. They're still the same concepts.
It's not much of a precedent -- there's no precedent in the spec, nor is it something widespread in other PLs. It exists in the ecosystem but even there Deferreds are not exactly the same thing as what
Promise.withResolvers
returns.
An impl in the most popular language library in the ecosystem. And in the precursor to the Promise spec, and the most famous promise library, a non-standard implementation in Chrome, internal names in projects like TS and node, official APIs in Deno. There is a naming precedent in our ecosystem, and even if the the exact object has slightly different properties, they're all the same pattern.
first option is a non-starter because the Promise constructor takes an executor as its only argument.
The executor needs to be callable, so there shouldn't be a compatibility problem if we add an overload.
if we add an overload.
JavaScript does not support overloading.
Sure it does:
function overloaded(arg) {
if (typeof arg === 'function') {
// do something
} else if (typeof arg === 'object' && arg !== null) {
// do something else
} else {
throw new TypeError;
}
}
@bakkot yes, you can simulate it by type checking like that. I knew someone was going to mention that, I should have preempted it 😆
more of an aside, but naming the wrapper object variable
promise
is confusing due to overload with staticPromise.resolve()
and leading topromise.promise.then()
I don't want the result be {promise, resolve}
either, I want it return a Promise with .resolve
property, so it's still promise.then()
.
I want it return a Promise with
.resolve
property
That's not going to happen. The language is not going to provide a helper which attaches the ability to resolve a Promise to the Promise itself.
If we go with overloading new Promise
(which is unlikely anyway), then we have to make sure new Promise instanceof Promise
. I think the same expectation exists with a method named create()
.
The language is not going to provide a helper which attaches the ability to resolve a Promise to the Promise itself.
Why it have to be attached, it can be a method on prototype, only if the promise is constructed in this way, it takes effect. (I know there will be "confusion" between Promise.resolve()
/Promise.p.resolve()
, but "so what")
can we keep the discussion here just focused on the naming, and open a separate issue for discussion of the return type/overloading the constructor? these are distinct questions.
An impl in the most popular language library in the ecosystem. And in the precursor to the Promise spec, and the most famous promise library, a non-standard implementation in Chrome, internal names in projects like TS and node, official APIs in Deno. There is a naming precedent in our ecosystem, and even if the the exact object has slightly different properties, they're all the same pattern.
ok fair enough, but i don't personally see that level of precedent as being persuasive in this case. presumably the value of keeping to precedent is you won't confuse people -- but as i said i think the opacity of the name is much more confusing to newcomers/casual users than the break in precedent would be to those who are familiar with the name.
I know there will be "confusion" between
Promise.resolve()
/Promise.p.resolve()
, but "so what"
For one awkward thing, MDN will not be able to document it at the moment😅 openwebdocs/project#104
Some may also expect p.resolve()
and Promise.resolve(p)
to be equivalent?
Personally, I'd say just drop the "with" at the front and just have:
const { resolve, reject } = Promise.resolvers();
I'm not sure if that collides with any existing methods, but I don't feel like the "with" is necessary unless it's there to explicitly avoid a collision with an existing method.
I've always called these "deferreds", so I'd be happy with
Promise.deferred()
I like this as well. We shouldn't be deterred by cognitive overloading from use of that term elsewhere/previously. And one could argue that it's actually beneficial.
Would predicating the returned value on a callback being passed allow for not adding a method at all?
const x = new Promise((resolve, reject) => {/* ... */})
// x = promise
// new way:
const x = new Promise()
// x = {promise, resolve, reject}
@mbrevda technically yes, but in actuality no, because that would mask an extremely common bug and so it wouldn't be deemed an acceptable design.
That suggestion wouldn't mask a bug, because the returned object isn't a promise at all, but rather a POJO. It would immediately break as soon as you tried to call any promise methods.
It's ruled out for a different reason - new Foo
should always return a Foo
. The language itself doesn't strictly enforce that (just that it returns a non-primitive), but everything in the entire web platform does so.
@tabatkins unless you await
ed it, in which case it would silently noop.
I agree with your other reason as well, of course :-)
What about Promise.postpone()
?
Words like postpone
and defer
suggest that, like setTimeout
, you are queuing some work to be done later. And that's not what this method does at all - it gives you a promise and the methods to resolve that promise. That's a pretty different thing.
Thanks all for explaining! Here's another non-method idea: add properties to the promise itself:
var promise = new Promise(/* ... */)
promise.then(/* */)
promise.resolve()
Is there something this would clash with?
@mbrevda Putting properties on the Promise itself means that the ability to read the Promise is conflated with the ability to write to the Promise, which is a very bad idea.
The referenced article makes an argument against the current design, too:
it [is] strange because you [are] constructing an object without using a constructor.
Although based on the above distinction (of internet vs external controll), perhaps some other method name ideas would include:
Promise.external()
// or
Promise.controlled()
// or
Promise.managed()
// or
Promise.extract()
// or
Promise.extrinsic()
@jnvm's suggestion looks very natural to me:
const { resolve, reject, promise } = Promise.create();
It's just another way to create a Promise
, similar, in a sense, to things like Array.from(thing)
.
Some thoughts:
Promise.resolvers
- confusing because it returns thereject
andpromise
, and doesn't indicate creationPromise.withResolvers
- confusing for same reasons, and longer (Edit: Although, per @peetklecha's comment below, this is actually closer to indicating creation - it's closer to the "sentence structure" ofArray.from(thing)
)(Edit: see @peetklecha's comment below)Promise.settlers
- still confusing because it also returns thepromise
, and most devs probably won't know what it meansPromise.defer
- vast majority of devs will have no idea what this means
Whatever the choice is, I think it's a good idea for the verb to indicate that something is actually being "created" - in the same way that it's clear that Array.from(thing)
is clearly creating an array.
I guess another possibility is to be even more explicit with something like (or Promise.create({withSettlers:true})
Promise.create({withResolvers:true})
), but that is maybe a little cumbersome - depends on how commonly this will be used I guess.
Promise.resolvers
- confusing because it returns thereject
andpromise
, and doesn't indicate creationPromise.withResolvers
- confusing for same reasons, and longerPromise.settlers
- still confusing because it also returns thepromise
, and most devs probably won't know what it means
To be very clear (and to also clarify something I mis-said during this past TC39 meeting), resolvers is the correct terminology. The resolve
function does not always settle the promise, it could also lock the promise in to the fate of another promise while leaving it in a pending state. The term resolve means: to determine the fate of the promise, i.e., to settle it directly or to lock it in to the fate of another promise. So fulfillment and rejection are both forms of resolution. So it is correct to say that resolve
and reject
are both resolvers.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise .
Whatever the choice is, I think it's a good idea for the verb to indicate that something is actually being "created" - in the same way that it's clear that
Array.from()
is clearly creating an array.
I'm glad you bring up Array.from
because it is inspiration for Promise.withResolvers
, namely the template <class>.<prepositionalphrase>
. In the same way that Array.from(thing)
gives you an array from thing, Promise.withResolvers()
gives you a promise with resolvers.
indeed, they are both called "resolving functions" in the spec
Promise.destructure()
from the OP hasn't gotten much attention. I don't hate it.
That implies to me the same thing as "unwrap", that it takes a promise and synchronously produces the result.
Curious how viable resolve and reject are as instance methods on Promise.
@lilnasy Not. See a few comments above.
I don't think I've seen anyone suggest Promise
called as a function:
const { promise, resolve, reject } = Promise();
It seems to meet all of our criteria without having to choose a method name.
...Except that overloading the call and construct signatures is probably a bad design post-ES6?
[[Call]]
and [[Construct]]
are different all the time, usually so one or the other will unconditionally throw. In a "post-ES6" world, it's like writing
constructor() {
if (new.target !== undefined) {
throw new TypeError;
}
// ...
}
It seems to meet all of our criteria without having to choose a method name.
perhaps.. my initial thought is that it induces the ick
wrt call vs construct, cognitive momentum, and potential for confusion.
is it better in some way than a named method on Promise
or is it just an anti-bikeshedding solution?
I think I'm starting to like it, but also seems like it would introduce a (minor) footgun, so maybe not
What's the footgun? That somebody accidentally uses new
?
const { promise, resolve, reject } = Promise()
JS is weird enough as it is around including new
vs not, and I think this would just add some more confusion/inconsistency for the average dev (like me). It looks to me like that should return a Promise
, rather than an object with a Promise
in it. Maybe there's a precedent for this sort of thing that I'm not aware of?
Also, I also agree with an earlier comment that Promise.destructure()
hasn't had enough attention - I initially thought it was overloading the term a bit too much, but, on reflection, it seems like an analogy that would be "permissible" to the mental models of most devs, and it avoids confusion around reject
actually being a "resolver", technically speaking.
I wouldn't mind if Promise.withResolvers moves forward as is. It's explicit, and it tells you about its relationship with Promise by being a static method on it.
If that isn't a constraint, however, I'd like to propose a new class.
const resolvable = new Resolvable()
{
...
resolvable.fulfill(value)
...
resolvable.reject(reason)
...
}
return resolvable.promise
Resolvable implies to me that other promises aren’t resolvable.
Getting back into the conversation here...
I get that withResolvers()
implies "promise and resolvers" now. With it being actually said, it kinda makes sense now. But I still think that it could very easily be confused.
I think that a better return value, to better fit the name, would be:
const [promise, { resolve, reject }] = Promise.withResolvers();
Further, at least to account for the other implication of the name, I think it maybe should [optionally] accept arguments for resolve and reject, defined externally to the promise:
function resolve() {
/*...*/
}
function reject() {
/*...*/
}
const [promise] = Promise.withResolvers(resolve, reject);
resolve('foo');
The former makes more sense to me with what the intended meaning is, and the latter is more along the lines of what I'd initially expect of it. I think that returning [promise, { resolve, reject }]
is reasonable and works well for both scenarios, and allows for resolve
and reject
to be passed in.
I also think that being able to pass in resolve
and reject
potentially make it more versatile and flexible since the conditions for resolve/reject could be imported and reused (eg a form submit or reject events, clicking certain buttons, a timeout/AbortSignal.timeout
etc).
I also think that being able to pass in resolve and reject potentially make it more versatile and flexible...
Wouldn't that be an alternate execution / subscription model and be out of scope for naming this helper utility?
external resolver thoughts
This inverts the promise in a way that unhelpfully hides how the promise mananges its state and invokes subscribers. I was thinking it would break (like couldn't work) but maybe I'm just not creative enough to figure out how to subscribe to a callback function being invoked. It's cleaner to return the resolver so the promise can hook into its invocations while managing its state rather than accept a callback and listen to this external callback's invocation.
Alsl what if resolve is invoked immediately? Do JS functions remember they are involved and would the promise instance now need to check that? Just seems far simpler to tie all resolvers existences to the promise instance directly and provide helpers to hook in after that, not before.
.deferred
/ .create
/ .withResolvers
wrap the current model with a noop helper that extracts the resolvers rather than providing an alternate model for creating or controlling a promise.
Could be off my rocker, happy to spar more, just seems like the kind of thing that 1) is not on point for this discussion and 2) would be dismissed almost off-hand either as an incompatible breaking change or as incurring additional complexity for implementors.
Thanks to everyone for participating in this discussion. Although I have never considered withResolvers
to be a perfect name, I haven't been compelled by any alternatives. The history-based defer
/deferred
has gotten the most discussion. Many other proposals have been suggested here but none have received any traction. So in advance of requesting Stage 3 at next week's TC39 plenary, I'm closing this issue and moving forward with withResolvers
.
To summarize my own rationale: withResolvers
(when read together with the class, which according to spec must be the receiver, barring subclassing cases) is totally descriptive of what it does: Promise.withResolvers
gives you a promise, along with its resolver functions. The only real alternative presented has been defer
/deferred
on the basis of those names having some historical usage in libraries. I think users who are already familiar with defer
will no trouble adapting to the new name and understanding what this method does. Users who are not already familiar with defer
, on the other hand, will be greatly benefitted, I think, by having a transparent, descriptive name.
Thanks again for everyone's participation!
I am not agree with a name #17.
Sorry I'm late to the party. If there's still a chance, consider "andResolvers" because to me, "withX" sounds like it adds an optional feature X to the Promise.
That ship kinda sailed https://groups.google.com/a/chromium.org/g/blink-dev/c/JL4uXtfrCdU/m/aK1t8LJMAAAJ