tc39 / proposal-promise-with-resolvers

Home Page:http://tc39.es/proposal-promise-with-resolvers/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Promise.defer history

ljharb opened this issue · comments

Promise.defer was a potential addition to ES6 that was not included. Chrome continued to ship it, until they eventually stopped doing so, since it wasn't part of the standard.

You may want to research es-discuss and add more background/history to the readme, since it's likely similar arguments for or against it will come up again.

Doing some archaeology on this... unfortunately, some of this wisdom of the ancients has been lost to history. For example, links to proposals that are now broken and no snapshot on archive.org.

From what I've been able to gather so far, Promise.defer() was dropped in favor of the Promise constructor for... reasons. Throw safety was mentioned in the context of Q.

Kris Kowal's perspective on it sheds some light.

Some example code in an obsoletion notice on old MDN docs shows Promise.defer() appear to be identical to this proposal:

var deferred = Promise.defer();
doSomething(function cb(good) {
  if (good)
    deferred.resolve();
  else
    deferred.reject();
});
return deferred.promise;

// would be:

return new Promise(function (resolve, reject) {
  doSomething(function cb(good) {
    if (good)
      resolve();
    else
      reject();
  });
});

Some people have mentioned "footguns" as justification for the removal of Promise.defer. Regardless of whether or not there's any historical accuracy to those claims, I find that justification ridiculous. You need to be careful doing async programming in general, and, as this proposal states and as discussed in plenary for Stage 1, this is an extremely common pattern.

To be clear, Promise.defer never existed in a final spec, so it was never removed - it's just that Chrome shipped it for years after ES6, and never championed its addition, and es6-shim will delete it if present.

The purpose of filing this issue was so the champion could attempt to locate arguments against including it - which so far, there seem to be none beyond "it might be unnecessary" - to ensure that the argument for adding it was persuasive.

From mozilla: https://bugzilla.mozilla.org/show_bug.cgi?id=1034599

Because ES6 standardized new Promise(function(resolve, reject){..}) as the way of constructing Promises, we should officially deprecate Promise.defer().

@ctcpip i can't find a version of firefox that shipped with Promise.defer, despite that link implying at least one exists. FF 29 introduced Promise and lacks defer, FF 30, 40, 50, 55, 56 all lack defer.

That seems to be talking about some alternative implementation, maybe for plugins or something?

Promise.jsm

This module was used before DOM Promises were made globally available in Gecko 29. Its usage is not suggested for new code.

https://blog.domenic.me/the-revealing-constructor-pattern/ points to reducing the chance of leaking internal details. Just because you have access to the promise doesn't mean you have the authority to resolve() that promise.

commented

this is an extremely common pattern

That doesn't mean that it's a good pattern, or a pattern that we should encourage by including a function in the spec.

There are in fact very good reasons why this pattern should be avoided. From my experience on StackOverflow, over 80% of cases where this pattern can be found should not be using the Promise constructor at all, and the remaining 20% could would do fine with using the executor pattern.

The current readme does not really give a rationale other than "it's popular". It does not really give any example how the pattern is used, and where or why it would be shorter/faster/simpler or in any other way objectively better than using the promise constructor normally.

Yes, the first link was mentioned earlier in this discussion. The pattern I was referring to is not the same as what you're linking to. Let's be absolutely clear about the pattern we are referring to:

let resolve;
let reject;
const myPromise = new Promise((resolve_, reject_) => {
  resolve = resolve_;
  reject = reject_;
})

That is not equivalent to a different pattern of nesting calls to functions that return promises within a promise constructor/executor.

Bad async code is bad async code and users can (and do) still implement this all the time without it being built-in. And nothing prevents a user from calling myPromise.then() in the example above.

More of an aside, but about 10 years ago, people were just getting to grips with promises, so stackoverflow issues from the era will reflect that. We also can guard against some bad async code via eslint, etc.

I agree with you that the proposal needs to be fleshed out with examples and such, but note that this just went from stage 0 to stage 1 last week. WIP. FWIW, eventing and channeling are the typical use cases IME. Here's one example

commented

The pattern I was referring to is not the same as what you're linking to.

Sorry, that link might have been misleading. I do understand what this proposal is about, the link to the promise constructor antipattern was meant as an example of a case where people are using new Promise and callbacks where a better API that natively returns promises is already available.

Bad async code is bad async code and users can (and do) still implement this all the time without it being built-in

So why encourage them writing bad async code by adding this built-in?

FWIW, eventing and channeling are the typical use cases IME. Here's one example

I'd argue that this should have been written differently as well:

export function request(type: string, message: any) {
  return new Promise((resolve, reject) => {
    if (socket) {
      socket.emit(type as any, message);
      socket.once('response', response => {
        if (response.status === 200) {
          resolve(response);
        } else {
          reject(response);
        }
      });
    } else {
      // probably wait on socket.
      throw 'No connection to server.';
    }
  });
}

(I wouldn't throw strings or response objects as exceptions either, but I've not changed that because it's not the point).
Possibly even simpler:

export async function request(type: string, message: any) {
  if (!socket) {
    // probably wait on socket.
    throw 'No connection to server.';
  }
  socket.emit(type as any, message);
  const response = await once(socket, 'response');
  if (response.status === 200) {
    return response;
  } else {
    throw response;
  }
}

I see that there are cases where the event source does not offer a once functionality, but these are increasingly rare.

Similarly, when you mention channels, these usually require a queuing mechanism to properly handle concurrent calls. Just setting an outer-scope variable doesn't cut it. The recommended pattern for request-response pairs over a channel would be

const resolvers: ((response: any) => void)[] = [];
socket.on('response', response => {
  // TODO: appropriately handle violations of the invariant that exactly one response is sent per received request
  const resolve = resolvers.shift();
  if (response.status === 200) {
    resolve(response);
  } else {
    resolve(Promise.reject(response));
  }
});
export function request(type: string, message: any) {
  return new Promise(resolve => {
    resolvers.push(resolve);
    socket.emit(type as any, message);
  });
}

When you do this, I do not see how

export function request(type: string, message: any) {
  const { promise, resolve } = Promise.withResolvers();
  resolvers.push(resolve);
  socket.emit(type as any, message);
  return promise;
}

is an improvement (and it does not reject the promise if emit throws an exception).

I don't agree that there is a connection between this proposal and bad async code. Your variations show there is more than one way to write functionally equivalent code. Still I don't see how a clear value proposition is extrapolated there. Nonetheless, we should probably create a separate issue to discuss the use cases that we are addressing, and can evaluate alternatives on their merits there.

https://blog.domenic.me/the-revealing-constructor-pattern/ points to reducing the chance of leaking internal details. Just because you have access to the promise doesn't mean you have the authority to resolve() that promise.

this still not addressed, probably the clearer/simpler argument against the proposal so far.

@y-nk That isn't an argument against this proposal at all - this proposal separates the Promise from the capability to resolve it. It's not going to provide resolve as a method on the Promise instance itself.

It'll expose the resolve function of the Promise outside of the scope of the promise's constructor, which is similar in fashion.

@y-nk this doesn't allow anything new that isn't already possible today. if you want to encapsulate or expose resolve/reject, you can do so if you like, and this proposal doesn't change that.

exposing resolve/reject outside of the promise executor does not happen automatically -- you have to deliberately write the code to do so. this just gives you a more direct route.

@ctcpip i think that was exactly the point of the OP of this comment (that's why i thought of answering here, it's quite an interesting argument and i thought it's sad it's been overlooked).

As i understand it, the design of the Promise as it is now shows intent that was the resolve/reject functions shouldn't spill outside of the Promise's constructor ; otherwise the Promise.deferred implementation wouldn't have been deprecated (, i think). It is true that, as you said, one can have an external variable to store those functions, but i guess it's equally fair to say it's not encouraged by the original implementation (for the reasons given above). therefore giving a way towards this pattern basically encourages a bad practice, don't you think?

@y-nk in speaking with the original authors and delegates, defer() was dropped only for scope reasons -- in other words, to reduce the size of the proposal. it's worth noting those authors explicitly support this proposal.

and as I have said previously, I don't agree that "this pattern basically encourages a bad practice".

edit: at the stage 3 meeting, it was mentioned that the original defer(red) was not dropped only for scope reasons. some history can be found in the notes from the stage 1 meeting:

MM: Yeah. I was there. I was actually the one that pushed for the – the original proposal actually had promise.defer. That was also in the proposal as I originally wrote it.
And I was the one who then pushed for the change to the current API, where you have the executor to the constructor. And I am glad that that – the executor and the constructor made it into the language, but the perspective that we had at the time was to be be as minimal as we can. So it was really one or the other. And I think that I agree that the aesthetics of language today is, although I am certainly very much still on the side of trying to err on the side of minimalism – I think to be redundant in functionality, by adding this common API, is fine.

@ctcpip thanks for the precisions 🙏

Deferred wasn't dropped for scope reasons lol, there is a ton of context in domenic/promises-unwrapping around it.

It was dropped for safety reasons since we didn't have async functions people would need to write code that had to .catch and also try/catch, this was for robustness and to prevent errors.

This isn't as big of a concern in a world you probably shouldn't use new Promise or Promise.withResolvers much anyway and already have a promisified API on most/all platforms. You can probably find much of the original discussion in the #promises channel - Domenic would certainly have the most knowledge/context on this.

IRC history from freenode is surely long since lost, unless someone has a copy somewhere.

I have a cold backup somewhere if we truly care but I absolutely remember these discussions, there were several others on GitHub domenic/promises-unwrapping#24 (comment)

It's not a big concern anymore because async functions but this does add a robustness/security footgun where users now have to remember to try/catch around their Promise.withResolvers code to avoid code sometimes throwing and sometimes rejecting. As long as people remember to use async functions it's mostly fine.

Also all the examples in the README should avoid both Promise.withResolvers and new Promise so it's probably better to replace them with good examples.

Additionally all the examples are wrong in how they promisify which is precisely userland code should strive to not promsify manually and if it does it should handle errors correctly.

Deferred wasn't dropped for scope reasons

Savaged again by the primary sources! I edited my comment above to reflect that this was mentioned at the meeting where this advanced to stage 3. I wasn't there (the time/place of the original defer(red) discussions), but I trust the folks who were there whom I spoke with about it. Memory is imperfect. Folks can chime in to try to patch these holes if they like. I don't think it would be terribly controversial for me to share, but I was contacted privately during the meeting about this, so I'm not comfortable repeating. And besides, I think I've done enough hearsay damage. 😄

@ctcpip I'm not angry or have any negative feelings towards you or your comment it's just amusing to see people trying to figure out this sort of stuff where everyone who was involved but now isn't is still around and a ping away :D

This was ±10 years ago everyone is still (hopefully) alive and coding stuff

Personally: The footgun is still there, async functions make it much smaller so personally I don't feel strongly about it.

To be absolutely clear, we spoke to folks who were involved and we had no reason to doubt what they said. (When I said 'savaged again by the primary sources', those are who I was referring to. Re-reading it now, it may appear I was referring to you. (I was not.))

In any case, my earlier comment that dropping defer(red) was only for scope reasons was reductive, and it would have been more helpful to have them chime in directly, or provide more context, quote them, etc.