The argument for avoiding promises for promises is fairly simple. A promise is a representation of a value that will exit in the future. If that value is still a promise, you haven't really got to the final value yet. Conceptually having the promise doesn't really provide anything other than a means to obtain the final value, as such it's of no additional use to be able to get the intermediate nested promises: You might as well just jump straight to the value.
This problem is explained in more detail by @erights here. The currently proposed [[Resolve]]
algorithm only dictates this recursive flattening for unrecognised thenables
, not for promises. THe advantage of this behavior is that a user can pick up a Promises/A+ library and return a deeply nested mess of foreign promises objects (all of which were broken enough not to do any unwrapping), and in a single step get back to the actual value being represented.
In the following example, you would ideally expect Q
to be equivallent to Q.then(identity)
var p = new Promise(resolve => resolve(5));
var q = new Promise(resolve => resolve.fulfill(pFor5));
var P = p.then(x => x); // P is a promise for 5, just like p
var Q = q.then(x => x); // oops! Q is a promise for 5, *unlike* q.
Promises are meant to make it easier to make synchronous code into asynchronous code. With this in mind we have to ask ourselves what the synchronous analog of any new feature would be. This is easiest to see in the case of a promise-for-a-rejected-promise.
A rejected promise from a function call represents that the function threw (asynchronously). What does a return value of a promise-for-a-rejected-promise represent? It seems to represent having called another internal function, that itself threw. This has no synchronous analog.
Similarly, of course, with return values: if f(x)
returns g(x + 1)
, and g(y)
returns y * y
, then f(x)
returns (x + 1) * (x + 1)
; it does not return some wrapped representation of g(x + 1)
that must then be unwrapped to get (x + 1) * (x + 1)
.
Another way of viewing the synchronous analog issue is that chaining a new .then
call is like adding another frame to the stack, this is easier to reason about if the promises are always fully flattened, rather than having a separate flatten operation. This argument does not really cause a problem for having then
callbacks that only unwrap one layer, it just makes clear that they must unwrap at least one layer.
The Parametricity argument in favour of promises for promises is essentially completely circular. It can be boiled down to:
- Promises for Promises should be allowed for everything because we should allow Promises for anything and not restrict the values a Promise can represent.
This isn't really an argument since it depends on it's conclusion to be true.
All in all, I would like to have both.
That is, I'm in favor of keeping current behavior of
then
for its simplicity in creating typical promise-chains.And I'm also much in favor of the idea that libraries support a way of not "eagerly" resolving promises. It can just be put in another method.