1

It is asserted the ECMAScript promises is a Promises/A+ implementation, so they have no contradictions. However, I encountered a behaviour of ecma promises which allegedly is out of line with the Promises/A+.

When we call promise1.then(onFulfilled, onRejected) to listen to the promise1's output, we get as a return value another promise (promise2). When the needed callback (onFulfilled/onRejected) was executed and it, in turn, returned some value x, the spec prescribes to resolve it with the defined [[Resolve(promise2, x)]] function. Let's suppose x happened to be a promise itself (x === promise3), then the steps must be taken is the following:

  • If x is a promise, adopt its state:
  • If x is pending, promise2 must remain pending until x is fulfilled or rejected.
  • If/when x is fulfilled, fulfill promise2 with the same value.
  • If/when x is rejected, reject promise2 with the same reason.

I wonder what if x is finally fulfilled with yet another promise (promise4) (there are not anything in the way of it, are there?). It can be concluded from the spec excerpt that promise2 must be fulfilled with promise4 too. But it is seemingly not so in the ECMAScript world:

let promise4 = new Promise((resolve) => { resolve(4) })

let promise3 = new Promise((resolve) => {
    resolve(promise4);
});

let promise1 = new Promise((resolve) => {
    resolve(1);
});

let promise2 = promise1.then((val) => { return promise3 });
promise2.then(val => console.log(val)); // output: 4

In the other words, promise2 is fulfilled with the promise4's value. This behaviour is like one that is defined in the spec for other thenable objects. So don't ECMAScript promises carry out expected type checking and just check whether x has then method?

Ilya Loskutov
  • 1,967
  • 2
  • 20
  • 34
  • _"...that `promise2` must be fulfilled with `promise4` too. But it is seemingly not so in the ECMAScript world"_ - As you mentioned yourself the output of `console.log(val)` is `4` which is the "value" of `promise4` so where's the problem? – Andreas May 26 '21 at 16:15
  • @Andreas `promise3` (`x`) fulfilled with `promise4` not with `4`. The spec says `promise2` must be fulfilled with the value of `x` not with the value of `promise4`. – Ilya Loskutov May 26 '21 at 16:20
  • `promise3` is `promise4`, `promise2` is `promise3` -> `console.log(val)` logs `4` the "value" of `promise4` – Andreas May 26 '21 at 16:25
  • @Andreas why is the value of `promise3` the value of `promise4` (that is `4`) and not the `promise4` itself? My misunderstanding is exactly here. Do you mean there is no difference between `resolve(4)` and `resolve(promise4)`? But I think it's not be drawed from the spec. Probably ECMAScript `resolve` cb is not a direct way to fulfill a promise I should delve into the ECMAScript spec.. – Ilya Loskutov May 26 '21 at 16:34
  • _"The [`Promise.resolve()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) method returns a `Promise` object that is resolved with a given value. If the value is a promise, that promise is returned; if the value is a thenable (i.e. has a ["`then`" method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then)), the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value."_ – Andreas May 26 '21 at 16:42
  • [Native `Promise`s do not provide a way to fulfill a promise with another promise](https://stackoverflow.com/q/32168194/1048572). Promises/A+ leaves this unspecified. It does, after all, implement the proper thenable resolution procedure though. – Bergi May 26 '21 at 18:01

2 Answers2

1

Let's suppose x happened to be a promise itself, then the steps must be taken is the following: […]

No, they don't need to be taken - they only may be taken if x is a "promise". These steps are an optional ("allowed", not "required") optimisation:

Note 4:
Generally, it will only be known that x is a true promise if it comes from the current implementation. This clause allows the use of implementation-specific means to adopt the state of known-conformant promises.

ECMAScript does not treat its own Promises as "known to be conformant", ignoring these steps. They simply treat native promises like all other thenables. Given there is no way to create an ECMAScript Promise that is fulfilled with another promise, this is equivalent to directly adopting the state.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • `2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).` There is no the word "must" too, does it mean `[[Resolve]]` is an optional function at all and an implementation may choose altogether different manner to process `x`? – Ilya Loskutov May 26 '21 at 19:05
  • @Mergasov No, the `then` method must follow that algorithm, but it can supply its own `IsKnownPromise` check. – Bergi May 26 '21 at 20:31
  • Actually, I incline to think of that "allow" word as concerning means (the spec gives permission for an implementation to decide on what's under the hood of its `IsKnownPromise`) but not as allowing to ignore all those steps completely which I'd rather consider to be obligatory. Nevertheless, I've got the answer ECMAScript promises behave differently. – Ilya Loskutov May 26 '21 at 21:03
  • 1
    @Mergasov Agreed. ES6' decision to define `IsKnownPromise = () => false` is weird, and prevents lots of useful optimisation possibilities. [Personally I consider it a bug](https://mail.mozilla.org/pipermail/es-discuss/2015-April/042517.html), but so far, nobody seemed to care. – Bergi May 26 '21 at 21:50
0

It can be concluded from the spec excerpt that promise2 must be fulfilled with promise4 too.

No, this does not follow from the Promises/A+ specification. The rule you quote from it

  • If/when x is fulfilled, fulfill promise2 with the same value.

...is recursive in nature. It should be understood in more elaborate terms as follows:

If/when x is fulfilled, fulfill promise2 with the same value that x fulfilled with.

Now for the part "that x fulfilled with", the same Resolution Procedure applies(!): Indeed, as x resolves with yet another thenable (promise4), it in turn gets locked-in with the next promise in the chain (promise4 in your example). This is not yet the fulfilled value. -- there is an important difference between resolving and fulfilling. This second execution of the Resolution Procedure will make sure that the value that x fulfils with, is the value that promise4 fulfils with.

The chain of locked-in promises can have any length, but the principle remains the same: each will resolve by locking into the next promise through this Promises/A+ Resolution Procedure. When the last one in this chain fulfils (with a non-thenable value), then all promises that are locked-in will get fulfilled with this value.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • The resolution procedure is imperative, a setter, not a getter that finds a fulfillment value. The recursion you mention goes the other way round. The is no recursion in the statement "*fulfill `promise` with the fulfillment value of `x`*" of `[[resolve]](promise, x)` - it is a base case. Sure, something else might run `[[resolve]](x, y)`, but that's unrelated. – Bergi May 26 '21 at 18:41
  • The resolution procedure is executed in a forward direction. I don't suggest that a fulfilment value is found via a getter. It is set by the last one in the chain, affecting all locked in promises. BTW, the Promises/A+ specification speaks itself of *"the recursive nature of `[[Resolve]](promise, thenable)`"*. – trincot May 26 '21 at 18:47
  • Yes, fulfilling one promise `x` does fulfill the other promises that were resolved with `x`, via the clause you quoted. But I don't see where the recursion is. The promise `x` might be resolved in any other way as well, not only through the promise resolution procedure - it only applies to promises created from `then()` calls. The recursive nature of the procedure refers only to resolving with a thenable (which might call the callback with another thenable argument). – Bergi May 26 '21 at 18:52