Skip to content
This repository has been archived by the owner on Aug 7, 2023. It is now read-only.

Proposal for hook into resolution attempts of already resolved/rejected promise #8

Closed
bmeurer opened this issue Jun 13, 2018 · 68 comments
Assignees
Labels
enhancement New feature or request

Comments

@bmeurer
Copy link
Member

bmeurer commented Jun 13, 2018

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

@bmeurer bmeurer added the enhancement New feature or request label Jun 13, 2018
@bmeurer bmeurer self-assigned this Jun 13, 2018
@benjamingr
Copy link
Member

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 .

@bmeurer
Copy link
Member Author

bmeurer commented Jun 13, 2018

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.

@benjamingr
Copy link
Member

@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.

@bmeurer
Copy link
Member Author

bmeurer commented Jun 13, 2018

@benjamingr sounds reasonable. I added another possible solution which would introduce a dedicated C++ hook just for this case.

@misterdjules
Copy link
Contributor

Without the ability to differentiate between programmer errors (bugs that materialize as an uncaught exception) and operational errors (reject is called with an error object), I'm not sure it's going to be possible for the embedder or a developer to determine whether the process should crash or not.

Would it be possible to add the ability to differentiate between those two different use cases?

@benjamingr
Copy link
Member

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 throw or a reject. I've seen multiple projects throw in the Promise constructor for operational stuff explicitly when doing the research for the summit.

Note that this hook is specifically for multiple throws/rejects and the current hook for promise rejections doesn't create a distinction. The line gets even blurrier in async functions (although that's not this issue).

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 thrown and not rejected for APMs or debugging then that's also worth discussing (again, I believe in a separate issue).


If I understand it correctly - this issue is specifically about detecting multiple rejects or throws in the promise constructor and providing a hook for that that APMs can hook into or Node.js can warn on.

@misterdjules
Copy link
Contributor

misterdjules commented Jun 13, 2018

Unfortunately programmer errors and operational errors in the Promise constructor are not always distinguishable by whether or not it's a throw or a reject. I've seen multiple projects throw in the Promise constructor for operational stuff explicitly when doing the research for the summit.

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.

Note that this hook is specifically for multiple throws/rejects and the current hook for promise rejections doesn't create a distinction. The line gets even blurrier in async functions (although that's not this issue).

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!

@benjamingr benjamingr changed the title Proposal to hook into swallowed promise rejections Proposal for hook into already resolved/rejected promise Jun 13, 2018
@benjamingr benjamingr changed the title Proposal for hook into already resolved/rejected promise Proposal for hook into resolution attempts of already resolved/rejected promise Jun 13, 2018
@bmeurer
Copy link
Member Author

bmeurer commented Jun 13, 2018

@misterdjules Added "Proposal for hook into already resolved/rejected promise" as subtitle to the design document. 👍

@getify
Copy link

getify commented Jun 13, 2018

this issue is specifically about detecting multiple rejects or throws in the promise constructor

Is there a separate issue for catching exceptions that happened in the promise constructor without the use of throw, like calling doesNotExist()? That is absolutely an important use-case for me.

Unfortunately programmer errors and operational errors in the Promise constructor are not always distinguishable by whether or not it's a throw or a reject.

Certainly that line may be blurry. But the line that shouldn't be blurry is whether an exception came from an explicit throw or whether it came from a runtime mistake in the programming. THAT distinction is very important to me. If that's a different issue or hook, fine. But I just wanted to make sure that wasn't getting glossed over.

@mhdawson
Copy link
Member

mhdawson commented Jun 13, 2018

Is it also possible to resolve after rejecting, needing a

kPromiseResolvedAfterReject = 3

@bmeurer
Copy link
Member Author

bmeurer commented Jun 14, 2018

@mhdawson I guess that'd be kPromiseResolveAfterResolved. The terminology is a bit odd here, but resolved for a promise actually means that it's no longer pending.

@bmeurer
Copy link
Member Author

bmeurer commented Jun 14, 2018

So as a promise state resolved means either fulfilled or rejected.

@benjamingr
Copy link
Member

@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.

@bmeurer
Copy link
Member Author

bmeurer commented Jun 14, 2018

@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 kPromiseResolveAfterResolved and kPromiseRejectAfterResolved should be sufficient then on our side?

@benjamingr
Copy link
Member

benjamingr commented Jun 14, 2018

Correct :)

bmeurer added a commit to v8/node that referenced this issue Jul 5, 2018
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
@bmeurer
Copy link
Member Author

bmeurer commented Jul 9, 2018

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.

@getify
Copy link

getify commented Jul 9, 2018

Is there a separate issue for catching exceptions that happened in the promise constructor without the use of throw, like calling doesNotExist()? That is absolutely an important use-case for me.

Ping.

@bmeurer
Copy link
Member Author

bmeurer commented Jul 9, 2018

@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?

@getify
Copy link

getify commented Jul 9, 2018

@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.

@bmeurer
Copy link
Member Author

bmeurer commented Jul 9, 2018

@getify I guess that should be possible to determine using this hook and the Error.stack property, which tells you where the exception originated from (i.e. you can tell from the source location whether it was an explicit throw inside of the executor or something else).

@getify
Copy link

getify commented Jul 9, 2018

@bmeurer I don't understand. Could you give a quick snippet of code illustrating what you mean?

@bmeurer
Copy link
Member Author

bmeurer commented Jul 10, 2018

@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`
}

@getify
Copy link

getify commented Jul 10, 2018

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 stack will give me the information I need. What kind of parsing am I going to have to do here?

@bmeurer
Copy link
Member Author

bmeurer commented Jul 10, 2018

Error.stack has column and line number inside, which you can use to find the code that generated the exception. Like if you run this in DevTools:

try { 1+Symbol.toPrimitive; } catch (e) { console.log("" + e.stack); }

it'll get back to you with something like

VM87:1 TypeError: Cannot convert a Symbol value to a number
    at <anonymous>:1:8

The <anonymous>:1:8 is the information you're interested in.

@bmeurer
Copy link
Member Author

bmeurer commented Jul 10, 2018

@BridgeAR, @benjamingr Do you want to look into adding the hook to Node?

targos pushed a commit to nodejs/node that referenced this issue Jul 19, 2018
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>
targos added a commit to nodejs/node that referenced this issue Jul 19, 2018
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>
targos added a commit to nodejs/node that referenced this issue Jul 19, 2018
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>
targos added a commit to targos/node that referenced this issue Jul 25, 2018
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>
targos added a commit to targos/node that referenced this issue Jul 25, 2018
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: 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>
targos added a commit to targos/node that referenced this issue Jul 26, 2018
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>
targos added a commit to targos/node that referenced this issue Jul 26, 2018
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: 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>
targos added a commit to targos/node that referenced this issue Jul 26, 2018
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>
targos added a commit to targos/node that referenced this issue Jul 26, 2018
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: 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>
@benjamingr
Copy link
Member

@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.

@larspars
Copy link

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:

return new Promise((resolve, reject) => {
    if (thing.isAThing) { resolve(thing); }
    if (thing.hasErrors) { reject(thing.accumulatedErrors); }
})

And only do it like this:

return new Promise(() => {
    if (thing.isAThing) { return thing; }
    if (thing.hasErrors) { throw new Error(thing.accumulatedErrors); }
})

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:

return new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("ok");
    }, 100);
})

Here, I propose that this is done with a new variant of setTimeout, i.e:

return new Promise(() => {
    return await setTimeout2(() => {
        return "ok";
    }, 100);
})

Here setTimeout2(fn) has the semantics that:

  1. It returns whatever fn returns.
  2. It rethrows whatever fn throws.
  3. It's awaitable

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:

return new Promise((resolve, reject) => {
    button.onclick = () => resolve("button was clicked");
})

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:

  • What conflicts, if any, exist with other parts of the current Promises API?
  • What's a good way to discourage use of reject/resolve? Issuing warnings when they are called for the first time?
  • Given that the reject/resolve design already exists, is it even worth it to push this other way of doing things?

@benjamingr
Copy link
Member

@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 new Promise and we want to provide hooks to improve debugging support for the promise constructor anyway just in case (like the case you raised in your article).


Ideally I want people to never have to use the promise constructor at all - that's why we have util.promisify and promise APIs. Ideally, people will use async functions and not "raw" promises in their code when possible and engines will provide better debugging tooling for those like async stack traces.

return new Promise(() => {
    if (thing.isAThing) { return thing; }
    if (thing.hasErrors) { throw new Error(thing.accumulatedErrors); }
})

Can literally be return and throw inside an async function.

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:

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.

(We do intend to do a survey to validate it)

Ideally - just like in .NET where people don't use TaskCompletionSource but use APIs that already return promises. We already have this experimentally for fs and dns is coming next. We also have async iterator (for... await) support for streams. Hopefully in a year or two we'll have such APIs for all the relevant APIs in Node.

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.

@larspars
Copy link

larspars commented Jul 29, 2018

@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.

@benjamingr
Copy link
Member

@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).

@larspars
Copy link

@benjamingr Sweet, I'll look over your links and let you know if I think I have anything semi-intelligent to contribute :)

@benjamingr
Copy link
Member

@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

MylesBorins pushed a commit to nodejs/node that referenced this issue Aug 10, 2018
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>
MylesBorins pushed a commit to nodejs/node that referenced this issue Aug 10, 2018
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>
BridgeAR added a commit to BridgeAR/node that referenced this issue Sep 18, 2018
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
targos pushed a commit to nodejs/node that referenced this issue Sep 25, 2018
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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants