-
Notifications
You must be signed in to change notification settings - Fork 6
Proposal for hook into resolution attempts of already resolved/rejected promise #8
Comments
The hook sounds good to me (Confirming Node.js can do it and that it is sufficient to address the use case in question and expose a hook from Node's side). Regarding the API - I'm not sure it should be the same function since no actual promise rejection is happening . |
It'd be convenient to re-use the existing C++ API, since it exposes everything we need. But that's not a requirement, we can also introduce a new C++ API if you think that's better. |
@bmeurer I think it's confusing to reuse the hook since at the moment it's only called when a promise rejects but I don't have very strong feelings about it. Also cc @misterdjules who has raised this concern about promises before and @getify who raised this issue as well. |
@benjamingr sounds reasonable. I added another possible solution which would introduce a dedicated C++ hook just for this case. |
Without the ability to differentiate between programmer errors (bugs that materialize as an uncaught exception) and operational errors ( Would it be possible to add the ability to differentiate between those two different use cases? |
Unfortunately programmer errors and operational errors in the Promise constructor are not always distinguishable by whether or not it's a Note that this hook is specifically for multiple I would love discussing how we can better distinguish programmer/operational errors and be more helpful towards programmers. (Although I think that should be a separate issue IMO). If you think there is value in the promise rejection hook exposing whether or not a promise was If I understand it correctly - this issue is specifically about detecting multiple |
That's good to know. Do you have more details to share about that specific part of your research (how it was done, your findings, etc.)? I think it could be useful to include in one of the documents in this repository for future reference.
Ah you're right, I was confused by the title of the issue. Reading the design document it's now clear that this is about a very specific use case. @bmeurer Do you think renaming this issue as "Proposal for hook into already resolved/rejected promise" would help clarify this? I agree in this case it's clear that there is a bug in the application and that developers/embedders can take appropriate action, including crashing. I also agree that being able to differentiate between promises rejected due to programmer or operational error should be discussed in a different issue. Thanks for the clarification @benjamingr! |
@misterdjules Added "Proposal for hook into already resolved/rejected promise" as subtitle to the design document. 👍 |
Is there a separate issue for catching exceptions that happened in the promise constructor without the use of
Certainly that line may be blurry. But the line that shouldn't be blurry is whether an exception came from an explicit |
Is it also possible to resolve after rejecting, needing a kPromiseResolvedAfterReject = 3 |
@mhdawson I guess that'd be |
So as a promise state resolved means either fulfilled or rejected. |
@bmeurer actually a promise resolved means either fulfilled, rejected or tracking another promise. All three are resolved: var p = Promise.resolve(); // resolved and fulfilled
var p2 = Promise.reject(new Error()); // resolved and rejected
var p3 = Promise.resolve(new Promise(() => {})); // p3 is resolved Resolved basically means "resolving it again would have no effect" in the context of this conversation. Fullfilled or rejected is called settled. |
@benjamingr Right. So the actual state of the promise can be queried from the object passed to the C++ API hook in case Node needs to know. Having |
Correct :) |
This is done in preparation for landing https://chromium-review.googlesource.com/c/v8/v8/+/1126099 on the V8 side, which extends the existing PromiseRejectEvent mechanism with new hooks for reject/resolve after a Promise was previously resolved already. Refs: nodejs/promises-debugging#8 Design: https://goo.gl/2stLUY
The Swallowed Rejection Hook just landed in V8 today (CL, v8:7919). We went for Solution 1 to keep the C++ API surface small. Not sure how urgent this is, but the changes are relatively easy, so you could even consider floating this as patch on top of Node 10 if you want the warning there. |
Ping. |
@getify To clarify: You're talking about exceptions that are raised inside of the executor function, but not via a syntactic throw inside the executor? |
@bmeurer Correct... IOW accidental exceptions, not exceptions you intentionally raised, specifically those occurring after you've already intentionally resolved (either fulfilled or rejected). These are currently "swallowed exceptions" in the truest sense of that concept. |
@getify I guess that should be possible to determine using this hook and the |
@bmeurer I don't understand. Could you give a quick snippet of code illustrating what you mean? |
@getify Once the swallowed rejection hook is exposed via Node, you should be able to hook into this: new Promise((res, rej) => {
// this will raise an error
return +Symbol.toPrimitive;
});
// Node will fire this hook
function rejectAfterResolved(reason) {
// Looking at reason.stack you'll see that the error didn't originate from a `throw`
} |
This is the part I don't understand, specifically. I'm not understanding how the |
try { 1+Symbol.toPrimitive; } catch (e) { console.log("" + e.stack); } it'll get back to you with something like
The |
@BridgeAR, @benjamingr Do you want to look into adding the hook to Node? |
This is done in preparation for landing https://chromium-review.googlesource.com/c/v8/v8/+/1126099 on the V8 side, which extends the existing PromiseRejectEvent mechanism with new hooks for reject/resolve after a Promise was previously resolved already. Refs: nodejs/promises-debugging#8 Design: https://goo.gl/2stLUY PR-URL: #21838 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [turbofan] Remove optimization of default Promise capability functions. The JSCallReducer could in theory inline the default resolve and reject functions passed to the executor in the Promise constructor. But that inlining is almost never triggered because we don't have SFI based feedback in the CallIC. Also the use of the Promise constructor is discouraged, so we shouldn't really need to squeeze the last bit of performance out of this even in the future. Getting rid of this optimization will make significantly easier to implement the Swallowed Rejection Hook, as there's less churn on the TurboFan side then. Bug: v8:7919 Change-Id: If0c54f1c6c7ce95686cd74232be6b8693ac688c9 Reviewed-on: https://chromium-review.googlesource.com/1125926 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{#54210} Refs: v8/v8@2075910 PR-URL: #21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [promise] Implement Swallowed Rejection Hook. This extends the current Promise Rejection Hook with two new events kPromiseRejectAfterResolved kPromiseResolveAfterResolved which are used to detect (and signal) misuse of the Promise constructor. Specifically the common bug like new Promise((res, rej) => { res(1); throw new Error("something") }); where the error is silently swallowed by the Promise constructor without the user ever noticing can be caught via this hook. Doc: https://goo.gl/2stLUY Bug: v8:7919 Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng Change-Id: I890a7e766cdd1be88db94844fb744f72823dba33 Reviewed-on: https://chromium-review.googlesource.com/1126099 Reviewed-by: Maya Lekova <mslekova@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Yang Guo <yangguo@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Cr-Commit-Position: refs/heads/master@{#54309} Refs: v8/v8@907d7bc PR-URL: #21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [turbofan] Remove optimization of default Promise capability functions. The JSCallReducer could in theory inline the default resolve and reject functions passed to the executor in the Promise constructor. But that inlining is almost never triggered because we don't have SFI based feedback in the CallIC. Also the use of the Promise constructor is discouraged, so we shouldn't really need to squeeze the last bit of performance out of this even in the future. Getting rid of this optimization will make significantly easier to implement the Swallowed Rejection Hook, as there's less churn on the TurboFan side then. Bug: v8:7919 Change-Id: If0c54f1c6c7ce95686cd74232be6b8693ac688c9 Reviewed-on: https://chromium-review.googlesource.com/1125926 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{nodejs#54210} Refs: v8/v8@2075910 PR-URL: nodejs#21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [promise] Implement Swallowed Rejection Hook. This extends the current Promise Rejection Hook with two new events kPromiseRejectAfterResolved kPromiseResolveAfterResolved which are used to detect (and signal) misuse of the Promise constructor. Specifically the common bug like new Promise((res, rej) => { res(1); throw new Error("something") }); where the error is silently swallowed by the Promise constructor without the user ever noticing can be caught via this hook. Doc: https://goo.gl/2stLUY Bug: v8:7919 Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng Change-Id: I890a7e766cdd1be88db94844fb744f72823dba33 Reviewed-on: https://chromium-review.googlesource.com/1126099 Reviewed-by: Maya Lekova <mslekova@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Yang Guo <yangguo@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Cr-Commit-Position: refs/heads/master@{nodejs#54309} Refs: v8/v8@907d7bc PR-URL: nodejs#21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [turbofan] Remove optimization of default Promise capability functions. The JSCallReducer could in theory inline the default resolve and reject functions passed to the executor in the Promise constructor. But that inlining is almost never triggered because we don't have SFI based feedback in the CallIC. Also the use of the Promise constructor is discouraged, so we shouldn't really need to squeeze the last bit of performance out of this even in the future. Getting rid of this optimization will make significantly easier to implement the Swallowed Rejection Hook, as there's less churn on the TurboFan side then. Bug: v8:7919 Change-Id: If0c54f1c6c7ce95686cd74232be6b8693ac688c9 Reviewed-on: https://chromium-review.googlesource.com/1125926 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{nodejs#54210} Refs: v8/v8@2075910 PR-URL: nodejs#21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [promise] Implement Swallowed Rejection Hook. This extends the current Promise Rejection Hook with two new events kPromiseRejectAfterResolved kPromiseResolveAfterResolved which are used to detect (and signal) misuse of the Promise constructor. Specifically the common bug like new Promise((res, rej) => { res(1); throw new Error("something") }); where the error is silently swallowed by the Promise constructor without the user ever noticing can be caught via this hook. Doc: https://goo.gl/2stLUY Bug: v8:7919 Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng Change-Id: I890a7e766cdd1be88db94844fb744f72823dba33 Reviewed-on: https://chromium-review.googlesource.com/1126099 Reviewed-by: Maya Lekova <mslekova@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Yang Guo <yangguo@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Cr-Commit-Position: refs/heads/master@{nodejs#54309} Refs: v8/v8@907d7bc PR-URL: nodejs#21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [turbofan] Remove optimization of default Promise capability functions. The JSCallReducer could in theory inline the default resolve and reject functions passed to the executor in the Promise constructor. But that inlining is almost never triggered because we don't have SFI based feedback in the CallIC. Also the use of the Promise constructor is discouraged, so we shouldn't really need to squeeze the last bit of performance out of this even in the future. Getting rid of this optimization will make significantly easier to implement the Swallowed Rejection Hook, as there's less churn on the TurboFan side then. Bug: v8:7919 Change-Id: If0c54f1c6c7ce95686cd74232be6b8693ac688c9 Reviewed-on: https://chromium-review.googlesource.com/1125926 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{nodejs#54210} Refs: v8/v8@2075910 PR-URL: nodejs#21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [promise] Implement Swallowed Rejection Hook. This extends the current Promise Rejection Hook with two new events kPromiseRejectAfterResolved kPromiseResolveAfterResolved which are used to detect (and signal) misuse of the Promise constructor. Specifically the common bug like new Promise((res, rej) => { res(1); throw new Error("something") }); where the error is silently swallowed by the Promise constructor without the user ever noticing can be caught via this hook. Doc: https://goo.gl/2stLUY Bug: v8:7919 Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng Change-Id: I890a7e766cdd1be88db94844fb744f72823dba33 Reviewed-on: https://chromium-review.googlesource.com/1126099 Reviewed-by: Maya Lekova <mslekova@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Yang Guo <yangguo@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Cr-Commit-Position: refs/heads/master@{nodejs#54309} Refs: v8/v8@907d7bc PR-URL: nodejs#21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
@larspars ping regarding your medium article - we acknowledge the problem you raised there of resolving after rejecting and are providing instrumentation for it :) Since you care about this do feel free to get involved. |
Thanks for the ping @benjamingr. I'm very much in favor of issuing a warning in these cases of double reject/resolve. In my post on error handling in Promises, I'm hinting at how I would have ideally liked this to be designed, but I guess I don't explain it in too much detail. I'm aware the following it's a fairly drastic proposal, but I'm not yet convinced it is crazy-talk. This will be a bit long, but I think it's relevant to the discussion. My take on this is that the complexities we're talking about here are not going to be top-of-mind for 99% of programmers using Promises. They'll be unaware of the case that you can do both reject and resolve on the same Promise. They'll be unaware of when a Promise rejection can be considered unhandled. Etc. So most programmers will be blind to a bunch of problems that may happen. The solution I propose is to throw away resolve/reject entirely, in favor of return/throw. Then you get language level support for there being only one of these happening for a given Promise. In other words, we stop doing this:
And only do it like this:
This removes a whole class of problems. But, it opens a new question: How would you implement rejects and resolves from async functions? I.e. how would you implement this:
Here, I propose that this is done with a new variant of setTimeout, i.e:
Here setTimeout2(fn) has the semantics that:
I initially thought this would need runtime support, but I think it's doable in user land. Then you don't need reject/resolve any more. There is a small number of use cases that go away. For example, you can't do this:
I think that's fine. There's a small loss of generality, in favor of a serious reduction in complexity. I think that's exactly what one should want from a design. It's exactly why array.forEach() is nicer than a straight for-loop, for example. It's also clear that such a design would work, because it's exactly the design of the .NET Task Parallel Library. The reduced complexity from this would be pretty massive. This entire discussion thread would be uneeded, and it seems like almost everything that is complex about Promises goes away. It's a fairly radical proposal. If this was five years ago, and Promises was not implemented in browsers, I would argue strongly in its favor. Today, I still think it's a better design, but it's possible the cost of switching to it may be too high. Actually removing resolve/reject seems like a no-go at this point, but perhaps one could move forward by downplaying resolve/reject in favor of using return/throw along with a new API for setTimeout(). Assuming everyone buys into this being a better design (big assumption), there are still some questions:
|
@larspars I think you're barging into an wide open door :) The tl;dr; is that we agree, we want to provide APIs so that people never have to write Ideally I want people to never have to use the promise constructor at all - that's why we have return new Promise(() => {
if (thing.isAThing) { return thing; }
if (thing.hasErrors) { throw new Error(thing.accumulatedErrors); }
}) Can literally be let delay = util.promisify(setTimeout); // Added in Node 8
async function doFoo() {
await delay(100);
// can `throw` or `return` here.
}
doFoo(); I couldn't agree more with:
(We do intend to do a survey to validate it) Ideally - just like in .NET where people don't use It is somewhat radical to provide promise APIs for everything - but given that's what people use and that's what the language does - we're still going to do it. |
@benjamingr Aha, sorry if I was barging in and stating what everyone was already thinking, haha. I haven't followed the discussions around this, but what you describe looks really nice, and looks like it would solve the problems I was talking about. |
@larspars if you have any other ideas please do let me know, personally I posted a canonical Q&A about it - proposed this hook in the Node collaborator summit, proposed .promisify (and pushed for it and bluebird before it was native) and am trying to promote the new promise APIs where I can. I don't think that's enough (or that we're moving quickly enough) which is why I think a lot of people are running into the same issues you are (and aren't writing blog posts about them or are as willing to interact). I personally don't believe we're in a very good place with promises and how we work with them (yet) - so any suggestions for hooks, debugging ideas etc. are welcome. Benedikt here is in V8 and while this is a node repository I would also happily connect you to relevant individuals in different browsers or the spec if you can prototype with hooks and provide feedback to implementors with CatchJS (or as an individual). |
@benjamingr Sweet, I'll look over your links and let you know if I think I have anything semi-intelligent to contribute :) |
@larspars sure, just don't assume we've thought of most of this stuff or have done so correctly - if you have ideas please do bring them up by opening an issue here or in nodejs/node |
Original commit message: [turbofan] Remove optimization of default Promise capability functions. The JSCallReducer could in theory inline the default resolve and reject functions passed to the executor in the Promise constructor. But that inlining is almost never triggered because we don't have SFI based feedback in the CallIC. Also the use of the Promise constructor is discouraged, so we shouldn't really need to squeeze the last bit of performance out of this even in the future. Getting rid of this optimization will make significantly easier to implement the Swallowed Rejection Hook, as there's less churn on the TurboFan side then. Bug: v8:7919 Change-Id: If0c54f1c6c7ce95686cd74232be6b8693ac688c9 Reviewed-on: https://chromium-review.googlesource.com/1125926 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{#54210} Refs: v8/v8@2075910 Backport-PR-URL: #21668 PR-URL: #21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
Original commit message: [promise] Implement Swallowed Rejection Hook. This extends the current Promise Rejection Hook with two new events kPromiseRejectAfterResolved kPromiseResolveAfterResolved which are used to detect (and signal) misuse of the Promise constructor. Specifically the common bug like new Promise((res, rej) => { res(1); throw new Error("something") }); where the error is silently swallowed by the Promise constructor without the user ever noticing can be caught via this hook. Doc: https://goo.gl/2stLUY Bug: v8:7919 Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng Change-Id: I890a7e766cdd1be88db94844fb744f72823dba33 Reviewed-on: https://chromium-review.googlesource.com/1126099 Reviewed-by: Maya Lekova <mslekova@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Yang Guo <yangguo@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Cr-Commit-Position: refs/heads/master@{#54309} Refs: v8/v8@907d7bc Backport-PR-URL: #21668 PR-URL: #21838 Refs: nodejs/promises-debugging#8 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org> Reviewed-By: Benedikt Meurer <benedikt.meurer@gmail.com>
This adds the `multipleResolves` event to track promises that resolve more than once or that reject after resolving. It is important to expose this to the user to make sure the application runs as expected. Without such warnings it would be very hard to debug these situations. Fixes: nodejs/promises-debugging#8
This adds the `multipleResolves` event to track promises that resolve more than once or that reject after resolving. It is important to expose this to the user to make sure the application runs as expected. Without such warnings it would be very hard to debug these situations. PR-URL: #22218 Fixes: nodejs/promises-debugging#8 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com> Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
As discussed during the summit, V8 could provide a hook to notify the embedder (Node/Chrome) about swallowed promise rejections, specifically with the
Promise
constructor (see Use Case #3 in promise-use-cases). I've written a design document how to extend the V8 C++ API here.cc @MayaLekova @benjamingr
The text was updated successfully, but these errors were encountered: