-
Notifications
You must be signed in to change notification settings - Fork 164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unspecified behavior when onFulfilled
or onRejected
throws a promise object
#65
Comments
Good catch. IMHO, this should always log 'REJECTED': promise.then(function () {
throw anyPendingFulfilledOrRejectedPromiseFor('REJECTED');
}).then(
function() { console.log('FULFILLED'); },
function(e) { console.log(e); }
); But I just checked and when.js simply hands the |
This should also get coverage in the test suite, once we decide what should happen :) |
@lsmith 👍 |
This actually is in the test suite, where Thus I think that indeed |
I do think clarifying the definition of |
@domenic I disagree for the same reasons that I don't like promise.then(function() { throw 123; });
// and
promise.then(function() { throw createAPromiseFor(123); }); Just as it is impossible to observe the difference between: promise.then(function() { return 123; });
// and
promise.then(function() { return createFulfilledPromise(123); }); Otherwise, we may break the identity function again: function success(x) { console.log('SUCCESS', x); }
function fail(x) { console.error('FAIL', x); }
// Logs SUCCESS 123
promise.then(function() { throw 123; })
.then(null, identity).then(success, fail);
// Logs FAIL 123
// The identity function should recover from a failure here because
// it does not explicitly propagate the rejection. However, it *unknowingly*
// (and probably mistakenly) propagates the rejection simply by returning its input
promise.then(function() { throw createRejectedPromise(123); })
.then(null, identity).then(success, fail); |
OK, after putting that on hold for a few days before reading it, that's a pretty compelling argument. @kriskowal, @wycats, @ForbesLindesay, @novemberborn, any objections? Given that Q and every other promise library seems to mistakenly propagate the rejection, we should make sure before we make this change. This, combined with #58, seem to me enough for a 1.1. If there are no objections by, say, Saturday, I'll give a go at making the changes, plus updating the website with new pages like revision history and list of conformant implementations. |
Wait. It's bugging me that let
It seems like I guess in my mind, |
This is needless fretting. Throwing a promise is outside the defined behavior of a promise system. |
@kriskowal I agree, we need to be careful not to make a spec that's needlessly difficult for new libraries to adhere to if it's an edge case that doesn't cause real compatibility issues. @domenic I think |
I'm +1 with @ForbesLindesay. |
While I agree with @kriskowal that throwing a promise is basically nonsensical, Javascript is not inherently a promise system, even though in practice, it is a highly asynchronous system. Practically speaking, for better or for worse, devs mix sync and async all the time. They'll throw whatever they want whenever they want, and they'll use sync libraries that do the same. So, I think we need to specify this for consistency. I share @ForbesLindesay's mental model that |
Which is close enough to how +1. |
Although I'm willing to get behind your proposed semantics for |
@domenic I agree that there's some tension there, given that we've used "reject" and "fulfill" in the promises-spec in a way that makes them sound parallel. However, I don't think that a polymorphic reject API for resolvers conflicts with our usage of the word "reject". Ultimately, it will always result in a rejected promise, whereas resolve was problematic because it might result in either a fulfilled or rejected promise. It seems like we're close to being ok with changing 3.2.6.2 to specify the additional cases for throwing a promise. Are there any other objections or concerns? |
This is a tough one.
If it's decided to wait for promises, I would suggest that Given there doesn't seem to be a clean solution here, I'm thinking erring on the side of de facto is preferable, because we want existing implementations that agree on the important things to be considered compliant rather than introducing a spec that nobody satisfies. That amounts to a loose +1 for passing the promise as the reason. If implementation authors widely agree that handling thrown promises is something worth changing their code for, then I'm fine with that. The other option of stating in a spec note that the handling of thrown promises is unspecified is undesirable, IMO. |
@lsmith Nice 2-bullet summary. The asymmetry between Of course, making I think I'd be ok with going with whatever seems simplest between rejecting with null/undefined, or with the "replacement" reason. My current thinking is that simply letting the replacement reason propagate would require no special casing, but maybe the code is simple for either. I'm also bristling slightly at squelching the later error, as I feel like it may be more helpful in tracking down a problem than null/undefined would be. Mostly that's speculation, though. |
As always I think we should look to sync code for what to do. function return5() { return 5; }
function throw5() { throw 5; }
try {
throw return5();
} catch (ex) {
assert(ex ===5);
}
try {
throw throw5();
} catch (ex) {
assert(ex ===5);
} |
@ForbesLindesay +1, that convinced me. |
Something is bothering me about the sync example. I don't think it's analogous, but I can't put my finger on where the discrepancy is. Regardless, I'm not passionate about either side and this is super edge casey, so whatever you guys agree on is fine by me. |
@lsmith It should be analogues to function return5() { return new FulfilledPromise(5); }
function throw5() { return new RejectedPromise(5); }
resolve()
.then(function () {
throw return5();
})
.catch(function (ex) {
assert(ex ===5);
});
resolve()
.then(function () {
throw throw5();
})
.catch(function (ex) {
assert(ex ===5);
}); |
@ForbesLindesay Very interesting. And for completeness: // This one is successful
resolve()
.then(function () {
return return5();
})
.catch(function (ex) {
assert(ex ===5);
});
// This one is not
resolve()
.then(function () {
return throw5();
})
.catch(function (ex) {
assert(ex ===5);
}); So, the only path to a successful result is returning a fulfilled promise (or non-promise, which is basically the same thing). |
I am suddenly curious how other languages' future/promise libraries handle this. Anyone want to do a test in their favorite language? I'll try to whip up C#, if I still remember how to do that. |
Ah of course, C# only allows you to throw things derived from |
@domenic As well we should pretend in JavaScript. |
That's def a valid question, and it'd be good if we could get some data points if possible. I agree it'd be nice if JS allowed throwing only Error instances or descendants, so we could ignore this situation. I feel like we need to specify something since JS allows throwing anything. |
I guess the question has to be asked: Should we even consider next-tick-throw-crashing in response to someone throwing a promise?? |
I don't think so, I think we should match Synchronous JavaScript as much as possible. That means we should let people throw whatever they like. But in our libraries we should only ever throw real exceptions. |
Anyone have any concerns before I merge #66? |
I'd like to get buy-in from @kriskowal and @wycats. |
Paging @kriskowal and @wycats for buy-in or concerns. |
A promise is not a valid exception. I won’t bloat Q with code to handle the case. |
Welp. Last try: @kriskowal, this is all about preventing promises-for-promises from being created, which I believe you in the past have expressed a desire to do. I guess your stance is that promises-for-promises are OK, if they're created via |
The case of a pending promise becoming a pending promise is not the same as a pending promise being rejected with a promise for an exception. I am content to omit |
@kriskowal when you say:
Do you mean precluding "values" from being exceptions? I often see libraries that "throw" plain objects. How do you intend to mandate? What should the behavior of the following be: function thows() {
return Q.resolve(null)
.then(function () {
throw Q.resolve(new Error('foo'));
});
} |
After talking about this with MarkM, I am convinced that our current specified behavior is fine. "Promises for promises" is a misnomer, because we are treating fulfillment and rejection too symmetrically. You can reject with any exception, even an exception that is a promise, and that's valid. Exceptions have no special semantics. You shouldn't be able to fulfill with a promise (and indeed, the base spec gives no method for doing this), since that's a true "promise for a promise," but rejecting with one is OK. Besides, lack of consensus kills this idea dead in the water. |
It may be dead due to lack of consensus, but I don't understand the reasoning behind some of the things you said:
Can you explain "too symmetrically" and why it is bad? Why is it valid to use promise-as-an-exception verbatim? |
I wish I could remember exactly how MarkM phrased it (@kriskowal may recall better), but essentially: promises represent eventual and/or remote values. A rejected promise takes you out of that paradigm, representing an eventual/remote thrown exception. The fulfillment value is primary, especially if you think of the remote object case: it's what's really underlying the promise. The exception is just a reason that the primary use case of the promise couldn't be fulfilled. Because we are focused on the |
- Separate out "promise" and "thenable" concepts. - Specify that "exception" just means "value". The first of these helps greatly with the ambiguity of defining exactly what a promise is, versus the operational definition of a "thenable" as something that a promise consumes and assimilates if returned from a handler. The second helps clarify #65.
Thanks. This is very interesting, and maybe somewhat of a lightbulb moment for me. It seems like what MarkM is saying is that we can look at a promise as being either fulfilled or not-fulfilled, which is perhaps subtly different from fulfilled, pending, or rejected. When it is not-fulfilled, it is either in the "fulfillment is possible", or "fulfillment is impossible" state. I am still uneasy about the weird behavior of the identity function for a thrown, fulfilled promise, or returning a rejected promise whose reason is a fulfilled promise (which would be used verbatim if reject is not polymorphic). This view of "fulfillment or not" kind of helps, though, since it removes the temptation to think about a rejection as function application on the rejection reason, while it still leaves the opportunity to think of fulfillment as function application on the fulfillment value, which imho, is a good thing. |
Hah, fair's fair :)
That's a really nice way of putting it, and I agree.
To be fair, so I am I. I take consolation in the fact that it's a crazy-weird edge case, as @kriskowal keeps emphasizing. |
- Separate out "promise" and "thenable" concepts. - Specify that "exception" just means "value". The first of these helps greatly with the ambiguity of defining exactly what a promise is, versus the operational definition of a "thenable" as something that a promise consumes and assimilates if returned from a handler. The second helps clarify #65.
- Separate out "promise" and "thenable" concepts. - Specify that "exception" just means "value". The first of these helps greatly with the ambiguity of defining exactly what a promise is, versus the operational definition of a "thenable" as something that a promise consumes and assimilates if returned from a handler. The second helps clarify #65.
Per my note on issue 7 of the resolvers spec, the spec language for 3.2.6.2 says
But "exception" isn't defined, so
is possible.
It's edge case city, but it is a loophole that could be considered a legal way of passing a promise as a value between callbacks (besides
return [otherPromise]
). Otherwise, it's an opportunity for implementation iniquity, and my guess would be that Unexpected Things are bound to happen in the pass-through mechanism when subsequentonRejected
callbacks aren't supplied.The text was updated successfully, but these errors were encountered: