Skip to content
This repository has been archived by the owner on Dec 12, 2023. It is now read-only.

Alternative names for withResolvers() #2

Closed
shgysk8zer0 opened this issue Mar 23, 2023 · 65 comments
Closed

Alternative names for withResolvers() #2

shgysk8zer0 opened this issue Mar 23, 2023 · 65 comments

Comments

@shgysk8zer0
Copy link

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 resolves 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?

@ljharb
Copy link
Member

ljharb commented Mar 23, 2023

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.

@jridgewell
Copy link
Member

I've always called these "deferreds", so I'd be happy with Promise.deferred()

@shgysk8zer0
Copy link
Author

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.

@jnvm
Copy link

jnvm commented Mar 23, 2023

const { promise, resolve, reject } = Promise.resolvers()

this mirrors Object.entries(), and doesn't risk any possible interpretational overload of the already existing with keyword.

@ljharb
Copy link
Member

ljharb commented Mar 23, 2023

Promises are a subset of Deferreds, so i'm not sure distance is required.

@peetklecha
Copy link
Collaborator

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.

@jnvm
Copy link

jnvm commented Mar 23, 2023

  1. Promise.evert() - "to turn inside out", though the visual closeness of "evert" to "event" is undesirable
  2. Promise.create() like Object.create()
  3. Promise.parts() similar to but not .entries()
  4. I'd suggest Promise.terminals() but {resolve, reject, promise} doesn't just have ends, instead 2 categories of things atm

@fisker
Copy link

fisker commented Mar 24, 2023

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;
      }
    );
  }
}

@ljharb
Copy link
Member

ljharb commented Mar 24, 2023

@fisker that would both mask and create bugs; it would be a very bad idea to accidentally expose a promise's resolvers.

@jridgewell
Copy link
Member

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.

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 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.

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.

@fisker
Copy link

fisker commented Mar 24, 2023

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?

@ljharb
Copy link
Member

ljharb commented Mar 24, 2023

@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.

@fisker
Copy link

fisker commented Mar 24, 2023

How do you think?

const promise = Promise.withResolvers();
promise.resolve(1);

That's a real "with resolvers", since resolvers are assigned to promise. 😄

@ljharb
Copy link
Member

ljharb commented Mar 24, 2023

@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.

@fisker
Copy link

fisker commented Mar 24, 2023

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);

@ljharb
Copy link
Member

ljharb commented Mar 24, 2023

That would certainly be a viable alternative.

@ctcpip
Copy link
Member

ctcpip commented Mar 24, 2023

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.

@ctcpip
Copy link
Member

ctcpip commented Mar 24, 2023

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()

@ctcpip
Copy link
Member

ctcpip commented Mar 24, 2023

per #1 it seems more and more like we are necromancing Promise.defer() and should consider leaning into that

@peetklecha
Copy link
Collaborator

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.

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.

@bakkot
Copy link

bakkot commented Mar 25, 2023

The spec calls these "capabilities", which suggests Promise.capability(), which... I don't hate, I guess. It's shorter than withResolvers, at least.

@jridgewell
Copy link
Member

The current proposal has it that there's no .then method on the thing returned by Promise.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.

@Josh-Cena
Copy link

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.

@ctcpip
Copy link
Member

ctcpip commented Mar 27, 2023

if we add an overload.

JavaScript does not support overloading.

@bakkot
Copy link

bakkot commented Mar 27, 2023

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;
  }
}

@ctcpip
Copy link
Member

ctcpip commented Mar 27, 2023

@bakkot yes, you can simulate it by type checking like that. I knew someone was going to mention that, I should have preempted it 😆

@fisker
Copy link

fisker commented Mar 27, 2023

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()

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().

@bakkot
Copy link

bakkot commented Mar 27, 2023

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.

@Josh-Cena
Copy link

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().

@fisker
Copy link

fisker commented Mar 27, 2023

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")

@aspirisen
Copy link

What about Promise.postpone() ?

@bakkot
Copy link

bakkot commented May 18, 2023

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.

@mbrevda
Copy link

mbrevda commented May 19, 2023

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?

@bakkot
Copy link

bakkot commented May 19, 2023

@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.

@mbrevda
Copy link

mbrevda commented May 19, 2023

The referenced article makes an argument against the current design, too:

it [is] strange because you [are] constructing an object without using a constructor.

@mbrevda
Copy link

mbrevda commented May 19, 2023

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()

@josephrocca
Copy link

josephrocca commented May 19, 2023

@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 the reject and promise, and doesn't indicate creation
  • Promise.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" of Array.from(thing))
  • Promise.settlers - still confusing because it also returns the promise, and most devs probably won't know what it means (Edit: see @peetklecha's comment below)
  • Promise.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 Promise.create({withSettlers:true}) (or Promise.create({withResolvers:true})), but that is maybe a little cumbersome - depends on how commonly this will be used I guess.

@peetklecha
Copy link
Collaborator

  • Promise.resolvers - confusing because it returns the reject and promise, and doesn't indicate creation
  • Promise.withResolvers - confusing for same reasons, and longer
  • Promise.settlers - still confusing because it also returns the promise, 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.

@ctcpip
Copy link
Member

ctcpip commented May 19, 2023

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.

@ljharb
Copy link
Member

ljharb commented May 19, 2023

That implies to me the same thing as "unwrap", that it takes a promise and synchronously produces the result.

@lilnasy
Copy link

lilnasy commented May 19, 2023

Curious how viable resolve and reject are as instance methods on Promise.

@bakkot
Copy link

bakkot commented May 19, 2023

@lilnasy Not. See a few comments above.

@michaelficarra
Copy link
Member

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.

@Josh-Cena
Copy link

...Except that overloading the call and construct signatures is probably a bad design post-ES6?

@michaelficarra
Copy link
Member

[[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;
  }
  // ...
}

@ctcpip
Copy link
Member

ctcpip commented May 22, 2023

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

@michaelficarra
Copy link
Member

What's the footgun? That somebody accidentally uses new?

@josephrocca
Copy link

josephrocca commented May 22, 2023

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.

@lilnasy
Copy link

lilnasy commented May 23, 2023

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

@ljharb
Copy link
Member

ljharb commented May 23, 2023

Resolvable implies to me that other promises aren’t resolvable.

@shgysk8zer0
Copy link
Author

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).

@snewell92
Copy link

@shgysk8zer0

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.

@michaelficarra michaelficarra mentioned this issue Jun 26, 2023
8 tasks
@peetklecha
Copy link
Collaborator

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!

@coderaiser
Copy link

coderaiser commented Jul 11, 2023

I am not agree with a name #17.

@mk-pmb
Copy link

mk-pmb commented Nov 14, 2023

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.

@mbrevda
Copy link

mbrevda commented Nov 14, 2023

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

No branches or pull requests