nodejs / CTC

Node.js Core Technical Committee & Collaborators
80 stars 27 forks source link

Support awaiting values through `util.awaitable` #12

Closed benjamingr closed 7 years ago

benjamingr commented 8 years ago

Adding actual promise support to Node has proven really difficult and a tremendous amount of work. In addition post-mortem concerns and other concerns proved to be a big burden. I think given the very large amount of work and the fact that the topic is highly debateable we should instead go with a simpler approach. async/await recently landed in v8.

I think we need a solution that gives users a version of the Node API that works with async/await but does not require us to touch the entire API piece by piece. I suggest we expose a util.awaitable method that takes a standard Node callback API and converts it to be consumable with async/await.

For example:

const fs = util.awaitable(require(fs));
async function foo() {
  const result = await fs.readFile("hello.txt"); // file contains "World"
  console.log("Hello", result);
}
foo(); // logs `Hello World`

util.awaitable(object | function)

If this method is passed an object, the method returns an object with the same method names -each of the methods returns an awaitable value (promise) instead of taking a callback.

If this method is passed a function, the method returns a function that does the same thing except it returns an awaitable value (promise).

Prior work

Bluebird has ~10 million monthly downloads on NPM and it provides a Promise.promisifyAll function which is very widely used. In addition other promise libraries like Q (7m downloads) also provide a similar function.

I suggested this solution back in 2014 but in 2014 we didn't have async/await finalized in the spec, v8 promises were slow and there was a much weaker case for adding it.

Why in core?

Promises and async/await are a language feature. It is impossible to implement util.awaitable without v8 private symbols so it can't be done in userland. The only way to do it fast from userland is to use a promise library like bluebird.


Basically I'm looking for some discussion around this. I feel like this is a much much simpler in terms of code added and maintenance load than actually changing the entire API to support promises which is the prior solution the CTC discussed and was in favor of.

Some "Promise people " might not be too thrilled about it - I'm not. I do however think it's a very reasonable compromise.

addaleax commented 7 years ago

@misterdjules Moving conversation from https://github.com/nodejs/node/pull/12442#issuecomment-294583167 here:

it’s just a fact that Promises are seeing widespread adoption, whether we explicitly support them in core or not (that we ship async/await now might be much more of a factor than this PR could be), so I personally don’t think it’s sensible to link this PR to the issues with debugging.

I don't understand that part of your response. Do you mean that the popularity of promises outweighs the issues they present with regards to existing best practices for error handling, including those that allow for using post-mortem debugging techniques?

What I meant is that Promise adoption happens and will continue to increase for the foreseeable future, whether we provide a direct API for them in core or not. And conversely, organizations with a no-Promises policy obviously won’t be forced to migrate off that in any case.

Adding this to core is about enabling users, and moving towards a standard way of accessing core’s APIs in a format that is already in widespread use.

Or, put another way: Right now, if people want to, they already use Promises. Adding promises support to core makes their lives, and the lives of people wanting to learn Node.js, easier, though.

Asking users to move to using async hooks seems like it could be highly disruptive for them.

One of the ideas that has been floated around was that once async_hooks is primetime-ready, domains could reasonably be re-implemented on top of them (It’s something that I think I’d really like to look into once #11883 lands). I’m not suggesting that moving from domains to bare async hooks would be easy in any way.

So I would think that if hooking promises to domain is not a large undertaking, it would be a good idea to at least investigate what it would take.

Not much, it seems: https://github.com/nodejs/node/pull/12489

benjamingr commented 7 years ago

@addaleax going to try giving getting some volunteers to look at the PR now and see if we can add some tests :)

bmeck commented 7 years ago

It’s something that I think I’d really like to look into once #11883 lands

Worries me for same reasons as stated in https://github.com/domenic/zones issues. If the intent here is a gateway to re-implement domains, I would like it to be stated explicitly.

addaleax commented 7 years ago

@bmeck Can you point to something more specific?

And, no, I think the intent of async_hooks is to provide a more low-level approach to tracking async operations. That domains might be re-implementable on top of that would just be a nice effect.

jasnell commented 7 years ago

@addaleax ... given the concerns on this.. how would you feel about marking this as experimental (assuming it lands) so that the specific implementation details can change without it being semver-major until we get more experience with the use?

bmeck commented 7 years ago

@addaleax

Fishrock123 commented 7 years ago

The idea was that domains should be one day implementable ontop of async_hooks so that we can remove the domains code that is everywhere. that being said, it is not explicitly built as a direct replacement of any form (it contains zero error handling at all). (You can handle errors by attaching and unattaching uncaughtException handlers.)

Aside: off-topic?

benjamingr commented 7 years ago

@bmeck definitely not a gateway to reimplement domains - all involved are aware of https://gist.github.com/trevnorris/d19addf4aa09a0a31a4a14e90cb5c302 and no one likes domains here :)

For what it's worth the complains @misterdjules raises here are all legitimate. I recall spending countless hours on this and postmortem concerns in the past.

That said I also agree with @addaleax about promisify being the best course of action at the moment, it's a much simpler approach than #5020 attempted and I think it's our best shot and we owe it to the community.

I don't think postmortem concerns can truly be addressed (even now that we have stack traces) until async stack chains land (not just async stack traces). Note that the promise constructor issues swallowing errors people complained about in #5020 - do not exist in this PR :)

addaleax commented 7 years ago

@jasnell I think that question might better go to @misterdjules as well. :smile: So far most of the concerns that have been voiced on the PR are about Promise support in core in general, and I don’t see that changing anytime soon.

That’s not to say I didn’t think of marking util.promisify as experimental as well … it’s just that so far I haven’t been able to see a notable advantage in that.

jasnell commented 7 years ago

The only notable example I see with marking it experimental would be in giving us more flexibility in exactly how it's implemented after we get more implementation experience with it.

mhdawson commented 7 years ago

I think being experimental has been helpful for N-API, allowing us to make changes to the API more easily early on, with the plan to make it non-experimental once things settle down based on implementation experience.

addaleax commented 7 years ago

@mhdawson Yea, I know what the experimental status is there for – and I’m not saying that we definitely won’t need to make adjustments to util.promisify(), it’s just that it looks to me like anything in that direction could be covered by adding options and sticking with normal semver rules. N-API has a lot more moving parts, I agree that it makes sense to use it there.

Are we willing to make the trade-off of safe error handling (and by association reliability and debuggability) versus popularity in Node core?

This PR means that we're unable to safely rely on the existing error handling semantics, namely not swallowing errors by default or affecting post-mortem debugging.

@yunong See also https://github.com/nodejs/CTC/issues/12#issuecomment-294896297, but basically the big question for me remains: How does a feature like this change the way companies like Netflix use Node? It is opt-in, so I would assume you are concerned about increasing ecosystem popularity of Promise-based code. I don’t want to clutter this discussion by repeating myself, but isn’t that a trend we are seeing anyway, and one that is more likely to be influenced by the existence of e.g. native async/await than by this feature?

And re: swallowing errors by default, I know about the issues with that, but I’d just like to draw your attention towards https://github.com/nodejs/node/pull/12010, a PR opened for throwing an error when an unhandled Promise rejection occurs (right now on garbage collection, but you’ll see in the thread that we’re still discussing the nuances there), as well as https://github.com/nodejs/node/pull/12489, which would add support for Promises to domains (which obviously still not the same as callback-based error handling, but still).

benjamingr commented 7 years ago

@yunong

This PR means that we're unable to safely rely on the existing error handling semantics, namely not swallowing errors by default or affecting post-mortem debugging.

Promises do not swallow errors and haven't since v1.3.4.

async function foo() {
  throw new Error("Hi");
}
foo(); // Unhandled Rejecton: "Hi"

This was fixed a long time ago in just because error swallowing is bad behavior. So I'm glad to tell you this particular concern has been addressed.

About postmortem - everything generally works - with https://github.com/nodejs/node/pull/12489. The only issue is that exceptions might result in a core dump one frame later, at which point relevant buffers to debugging might have been touched. I don't think it's that bad since it's only a microtick so no other events have been handled by the time it happens. This again is a deliberate decision for this.

Promises are a fact at this point, more than half of our users use them if we believe surveys and the current state of affairs is very problematic. I, and tens of thousands of others have promises in production and have been able to debug our code (and take core dumps) just fine - what you're describing has been mostly fixed :)

As for your code, no one is suggesting that you use promises all of a sudden - the goal is to enable users to use a certain language feature (async/await) rather than force them to use language features more slowly from userland. This is about enabling our majority of users to use the core APIs with what the language gave us.

I realize we're not all fans of promises, and I think the concerns you raise are important - but as a first step landing just enough promise support for enabling our users but without forcing anyone to use promises is amazing.

misterdjules commented 7 years ago

@benjamingr

This PR means that we're unable to safely rely on the existing error handling semantics, namely not swallowing errors by default or affecting post-mortem debugging.

Promises do not swallow errors and haven't since v1.3.4.

  async function foo() {
    throw new Error("Hi");
  }
  foo(); // Unhandled Rejecton: "Hi"

This was fixed a long time ago in just because error swallowing is bad behavior. So I'm glad to tell you this particular concern has been addressed.

I believe that what @yunong meant by "not swallowing errors" is a bit stricter than emitting a warning at some point in the future. In your example above, the error is still handled (effectively "caught") and emitted later in a different form.

I agree it's better than nothing, but I think it might not suit the use case of users who expect the process to exit fast (that is, before a microtask, a nextTick callback, or other async operations have time to run).

FWIW, I had documented in a gist what I think constitutes the fundamental requirements for promises to not break post-mortem debugging support.

About postmortem - everything generally works -

I disagree, although I'd like to be proven wrong!. Let's consider this example:

[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# cat /var/tmp/throw-in-async.js 
function PostmortemDebuggingState() {
  this.state = [];
}

process.on('unhandledRejection', function onUnhandledRejection() {
  process.abort();
});

var postmortemDebuggingState = new PostmortemDebuggingState();

async function foo() {
  postmortemDebuggingState.state.push('foo');
  throw new Error("Hi");
}

foo();

process.nextTick(function bar() {
  postmortemDebuggingState.state.push('bar'); 
});
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# node --version
v7.2.1
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# node --harmony-async-await /var/tmp/throw-in-async.js 
 1: node::Abort() [/opt/local/bin/node]
 2: node::Abort(v8::FunctionCallbackInfo<v8::Value> const&) [/opt/local/bin/node]
 3: v8::internal::FunctionCallbackArguments::Call(void (*)(v8::FunctionCallbackInfo<v8::Value> const&)) [/opt/local/bin/node]
 4: v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/opt/local/bin/node]
 5: v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/opt/local/bin/node]
 6: v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) [/opt/local/bin/node]
 7: 2cd02a5063a7
 8: 2cd02a645b4d
 9: 2cd02a507e55
10: 2cd02a61ced7
11: 2cd02a61cb05
12: 2cd02a507e55
13: 2cd02a64563f
14: 2cd02a644f9d
15: 2cd02a644af2
16: 2cd02a644787
17: 2cd02a62e219
18: 2cd02a62e04d
19: 2cd02a57c62b
20: 2cd02a578b1a
21: 2cd02a54a7c3
22: 2cd02a52aa81
23: v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, bool, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*, v8::internal::Handle<v8::internal::Object>) [/opt/local/bin/node]
24: v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*) [/opt/local/bin/node]
25: v8::Function::Call(v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) [/opt/local/bin/node]
26: v8::Function::Call(v8::Local<v8::Value>, int, v8::Local<v8::Value>*) [/opt/local/bin/node]
27: node::LoadEnvironment(node::Environment*) [/opt/local/bin/node]
28: node::Start(uv_loop_s*, int, char const* const*, int, char const* const*) [/opt/local/bin/node]
29: node::Start(int, char**) [/opt/local/bin/node]
30: _start [/opt/local/bin/node]
Abort (core dumped)
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# echo "::load  /root/mdb_v8_amd64.so; ::findjsobjects -p state | ::findjsobjects | ::jsprint" | mdb /var/tmp/core.node.71546 
mdb_v8 version: 1.1.4 (release, from 3a6fad0)
V8 version: 5.4.500.44
Autoconfigured V8 support from target
C++ symbol demangling enabled
{
    "state": [
        "foo",
        "bar",
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
    ],
}
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]#

What happens is that the data that was used to store the state meant to be inspected with post-mortem debugging tools was modified after the error was thrown. In this case, the program adds to the data meant to be inspected post-mortem, which could already make root causing the original problem more difficult, but in general that data could be modified in any way, e.g deleted.

Did I miss something?

with nodejs/node#12489.

My understanding is that nodejs/node#12489 is not relevant to post-mortem debugging. Domains are orthogonal with post-mortem debugging. Using one does not impact one's ability to use the other. For the sake of keeping this discussion focused on the original topic, I would suggest not discussing that PR further in this issue.

The only issue is that exceptions might result in a core dump one frame later,

What do you mean by "frame"? Do you mean a stack frame, a libuv event loop tick, a V8 microtasks tick, something else?

at which point relevant buffers to debugging might have been touched. I don't think it's that bad since it's only a microtick so no other events have been handled by the time it happens.

Wouldn't other (as in, more than 0) microtasks be able to run between the time the error is thrown and the 'unhandledRejection' event is emitted?

Let's consider the same example as above, but modified to run another async function after the error is thrown in the first async function called:

[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# cat /var/tmp/throw-in-async.js 
function PostmortemDebuggingState() {
  this.state = [];
}

process.on('unhandledRejection', function onUnhandledRejection() {
  process.abort();
});

var postmortemDebuggingState = new PostmortemDebuggingState();

async function foo() {
  postmortemDebuggingState.state.push('foo');
  throw new Error("Hi");
}

async function baz() {
  postmortemDebuggingState.state.push('baz');
}

foo();
baz();

process.nextTick(function bar() {
  postmortemDebuggingState.state.push('bar'); 
});
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# node --harmony-async-await /var/tmp/throw-in-async.js 
 1: node::Abort() [/opt/local/bin/node]
 2: node::Abort(v8::FunctionCallbackInfo<v8::Value> const&) [/opt/local/bin/node]
 3: v8::internal::FunctionCallbackArguments::Call(void (*)(v8::FunctionCallbackInfo<v8::Value> const&)) [/opt/local/bin/node]
 4: v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/opt/local/bin/node]
 5: v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/opt/local/bin/node]
 6: v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) [/opt/local/bin/node]
 7: dc2153063a7
 8: dc215447ccd
 9: dc215307e55
10: dc21541ced7
11: dc21541cb05
12: dc215307e55
13: dc2154477bf
14: dc21544711d
15: dc215446c72
16: dc215446907
17: dc21542e219
18: dc21542e04d
19: dc21537c62b
20: dc215378b1a
21: dc21534a7c3
22: dc21532aa81
23: v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, bool, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*, v8::internal::Handle<v8::internal::Object>) [/opt/local/bin/node]
24: v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*) [/opt/local/bin/node]
25: v8::Function::Call(v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) [/opt/local/bin/node]
26: v8::Function::Call(v8::Local<v8::Value>, int, v8::Local<v8::Value>*) [/opt/local/bin/node]
27: node::LoadEnvironment(node::Environment*) [/opt/local/bin/node]
28: node::Start(uv_loop_s*, int, char const* const*, int, char const* const*) [/opt/local/bin/node]
29: node::Start(int, char**) [/opt/local/bin/node]
30: _start [/opt/local/bin/node]
Abort (core dumped)
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]# echo "::load  /root/mdb_v8_amd64.so; ::findjsobjects -p state | ::findjsobjects | ::jsprint" | mdb /var/tmp/core.node.6732 
mdb_v8 version: 1.1.4 (release, from 3a6fad0)
V8 version: 5.4.500.44
Autoconfigured V8 support from target
C++ symbol demangling enabled
{
    "state": [
        "foo",
        "baz",
        "bar",
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
        hole,
    ],
}
[root@15f20df8-70ef-ec46-a085-b7205401ddc7 ~]#

It seems that the new async function baz ran, and was able to mutate the state that is meant to be inspected post-mortem, which seems to be a problem.

As for your code, no one is suggesting that you use promises all of a sudden - the goal is to enable users to use a certain language feature (async/await) rather than force them to use language features more slowly from userland. This is about enabling our majority of users to use the core APIs with what the language gave us.

I realize we're not all fans of promises, and I think the concerns you raise are important - but as a first step landing just enough promise support for enabling our users but without forcing anyone to use promises is amazing.

My concern is about the impact of node's built-in APIs supporting the promises' error handling model by providing a way for its users to use the built-in APIs as promises.

I believe that this would create a significantly higher risk for post-mortem debugging users to indirectly use third party modules that use promises, and thus use an error handling model that is not compatible with their use case.

As a result, I think that implementing what is described in this issue, without solving the fundamental difference between the promises and non-promises error handling models, will have a significantly negative impact on the post-mortem debugging use case.

I would like to clarify my position by stating that I would much rather not oppose the addition of promises-based APIs in node's built-in API, and I'd be more than happy to change my current position. But unless we can show that all significant post-mortem debugging use cases are addressed, I'll have to consider that we're willing to break node's post-mortem debugging support. I don't think that this would be a good message to send to our users in general.

bnoordhuis commented 7 years ago

What happens is that the data that was used to store the state meant to be inspected with post-mortem debugging tools was modified after the error was thrown.

The asynchronous delivery of the 'unhandledRejection' event - while good for preventing runaway recursion - is not helpful for postmortem tooling, I agree.

Perhaps a good way forward is to extend --abort_on_uncaught_exception to also cover unhandled rejections.

misterdjules commented 7 years ago

@addaleax

I don't want to reply for @yunong, but as a post-mortem debugging user myself.

How does a feature like this change the way companies like Netflix use Node? It is opt-in, so I would assume you are concerned about increasing ecosystem popularity of Promise-based code.

It is opt-in, but adding what this issue describe to node's built-in API would make the node platform officially support an error model that is incompatible with some of the best practices that have been developed, and that enable the post-mortem debugging use case.

So my concern is that it would make it more difficult than it already is for post-mortem debugging users to not run code that is incompatible with their use case.

I don’t want to clutter this discussion by repeating myself, but isn’t that a trend we are seeing anyway, and one that is more likely to be influenced by the existence of e.g. native async/await than by this feature?

I agree it is a trend that we're seeing, but at the same time the node runtime is a compelling platform for some users because it allows them to use an error model compatible with post-mortem debugging.

I find it disturbing that it seems the node project is considering to break the post-mortem debugging use case without putting a lot of effort on finding a solution to make promises work with that use case.

And re: swallowing errors by default, I know about the issues with that, but I’d just like to draw your attention towards nodejs/node#12010, a PR opened for throwing an error when an unhandled Promise rejection occurs (right now on garbage collection, but you’ll see in the thread that we’re still discussing the nuances there)

I would think that throwing an error on garbage collection of rejected promises has the same issues than the ones I presented above. Among other potential problems, it seems the process would exit at a time that is not necessarily the time at which the error was thrown, and as a result the generated core files might not be usable for post-mortem debugging.

I believe these potential problems and others have been presented by @chrisdickinson in that PR already.

I'd be happy to be convinced that this PR solves all the post-mortem debugging concerns and change my mind though.

as well as nodejs/node#12489, which would add support for Promises to domains (which obviously still not the same as callback-based error handling, but still).

Right, that PR is orthogonal to the post-mortem debugging concerns, I don't think it helps to discuss it further here.

misterdjules commented 7 years ago

@bnoordhuis

Perhaps a good way forward is to extend --abort_on_uncaught_exception to also cover unhandled rejections.

I had started to explore solutions (with the help of others) at https://gist.github.com/misterdjules/2969aa1b5e6440a7e401#potential-solutions.

I had stopped that work when https://github.com/nodejs/node/pull/5020 was closed without being merged, and IIRC the state of that work at the time was that I couldn't find a way to have a "--abort-on-unhandled-rejection" mode that would be compatible with the normal mode.

In other words, code that would not exit when throwing an error from within a promise would then make the process abort when running node with --abort-on-unhandled-rejection.

But this was before https://github.com/nodejs/node/pull/8217 landed, and maybe now this design problem doesn't exist anymore if the node runtime moves towards exiting on unhandled promises rejection.

bnoordhuis commented 7 years ago

Shouldn't be too hard to make it work though, right? Check if --abort_on_uncaught_exception is specified in node.cc and in unhandledRejection() in lib/internal/process/promises.js call process.abort() if it is.

misterdjules commented 7 years ago

@bnoordhuis

Shouldn't be too hard to make it work though, right?

It seems it would at least be possible, and probably not difficult, I agree.

Check if --abort_on_uncaught_exception is specified in node.cc and in unhandledRejection() in lib/internal/process/promises.js call process.abort() if it is.

It seems that method would rely on using SetPromiseRejectCallback. At the time I worked on this, I had found potential problems with using that method. I don't know if they still exist.

bnoordhuis commented 7 years ago

Sorry, I was perhaps a little too succinct there. What I mean is that:

  1. You scan the command line for the --abort_on_uncaught_exception flag. There is precedence for that, we already do it for a few other V8 flags.

  2. Expose that fact to JS land, e.g. by adding a fourth argument to the process._setupPromises() callback. (A little neater than adding another property to process and leaves the door open for making it configurable at runtime.)

ljharb commented 7 years ago

I would not recommend conflating "uncaught exceptions" from "unhandled rejections" - Promise.reject() is not an exception, but it is definitely an unhandled rejection. A separate flag would be much more explicit.

misterdjules commented 7 years ago

@bnoordhuis

by adding a fourth argument to the process._setupPromises() callback

I want to make sure I understand what you mean. Do you mean making PromiseRejectCallback abort in case the flag passed to process._setupPromises() indicates that the runtime should abort on an unhandled promise rejection?

If that accurately describes what you're suggesting, I still think it would not fix the problems with the post-mortem debugging use case. The reason is that the PromiseRejectCallback function is called asynchronously, after all microtasks that had been scheduled have run when the stack frame where an error was thrown is not at the top of the stack. At least it is how it used to behave when I tried to use it for that purpose last time. Do you mean that that behavior changed, or am I missing something?

benjamingr commented 7 years ago

@misterdjules

I believe that what @yunong meant by "not swallowing errors" is a bit stricter than emitting a warning at some point in the future. In your example above, the error is still handled (effectively "caught") and emitted later in a different form.

The above example dispatches an event that can be handled with a throw at unhandledRejection before any I/O has run. Note that there is a PR by @Fishrock123 to make it throw and abort on GC which would exit the process.

I agree it's better than nothing, but I think it might not suit the use case of users who expect the process to exit fast (that is, before a microtask, a nextTick callback, or other async operations have time to run).

I'm not sure I agree, the vast majority of promise users (who are a majority of Node users as a whole if we believe surveys) do not really use core dumps at all, and out of those who do - the number of cases a microtask passing has any meaningful consequence is much smaller. Again, this is entirely opt in and this PR just enables users to use a language feature directly.

I disagree, although I'd like to be proven wrong!. Let's consider this example:

I'd like to see a realistic use case where a microtask isn't artificially used to insert something before I/O callbacks. With a setImmediate or anything that would indicate any data processing that would not be the case. Of course I'd just pass a copy of the array with an error at which point we are able to avoid having to take a core dump altogether.

What do you mean by "frame"? Do you mean a stack frame, a libuv event loop tick, a V8 microtasks tick, something else?

I meant the microtask queue gets to run, not an event loop tick.

Let's consider the same example as above, but modified to run another async function after the error is thrown in the first async function called:

Right, as I said the microtask queue is depleted, I am not suggesting this does not cause errors to be taken as core dumps later - I'm saying that the benefit of said tooling is something the majority of our users care a lot less about than having a core API they can use with async/await.


It is opt-in, but adding what this issue describe to node's built-in API would make the node platform officially support an error model that is incompatible with some of the best practices that have been developed, and that enable the post-mortem debugging use case.

I disagree it's the "best practice", I think we have established in https://github.com/nodejs/post-mortem/issues/18 that it's not really possible to take a core dump responsibly on every error that happens anyway.

Mainly because:

I believe that this would create a significantly higher risk for post-mortem debugging users to indirectly use third party modules that use promises, and thus use an error handling model that is not compatible with their use case.

The Node ecosystem is already full of modules that use promises. Bluebird has 14 million downloads and dependents like some of the most popular ORMS, build tools and server infrastructure tools. Similarly for Q (with 11 million). Note I'm ignoring native promises which are probably much more popular at this point.

This PR is just about making a utility that enables users to easily get a version of the API that's safe and fast for supporting async functions (which people use anyway). Making people get a bunch of third party tools to use our core APIs the same way they use every other API is making Node look bad and is costing a lot of programmers a lot of time.

If you want to blacklist them you can still delete Promise. This PR does not add promise support to the core APIs, it just enables users to do it easily and performantly.

bnoordhuis commented 7 years ago

The reason is that the PromiseRejectCallback function is called asynchronously, after all microtasks that had been scheduled have run when the stack frame where an error was thrown is not at the top of the stack.

I think you already figured this out but for posterity: the event is synchronous but promises.js defers it with process.nextTick().

bmeck commented 7 years ago

@bnoordhuis yes and it must not cause an abnormal completion according to spec: https://tc39.github.io/ecma262/#sec-host-promise-rejection-tracker

jasnell commented 7 years ago

The specific opt-in on this is what makes it the most attractive. I fully understand that there are folks who have deep technical objections to the design and use of Promises in the language, the way they have been implemented, and the limitations they have with regards to Post mortem debugging, but having a util.promisify() function added to core does not force anyone to make use of those things. It's easily possible to blacklist the use of Promises in an application and the existing APIs continue to work as they have always done. If this effort was forcing folks to make use of Promises, then I definitely would not be supportive, but it's not... nor is it trying to solve all of the problems that exist with Promises as currently defined.

littledan commented 7 years ago

A couple notes--happy to see this getting more complete with @addaleax 's PR:

benjamingr commented 7 years ago

@littledan a review of the PR would be much appreciated :) I'm not too concerned about performance not being better than bluebird initially since we can always ship fast promisify and make it faster in a follow up.

I've benchmarked the PR (stupidly, in my own app benchmark and not with the BB suite) and it significantly outperformes a naive new Promise which is good enough for me at this point. I intend to test util.promisify in our (BB) suite sometime in the coming weeks and validate this.

Thanks for all the performance work you (V8) have been doing on making promises fast enough for Node users by the way :) !

chrisdickinson commented 7 years ago

@ljharb:

I would not recommend conflating "uncaught exceptions" from "unhandled rejections" - Promise.reject() is not an exception, but it is definitely an unhandled rejection. A separate flag would be much more explicit.

To clarify: in arguing for "crash on unhandled rejection", it is important to note that Promise.reject() would not immediately crash the program. The unhandled rejection handler is not called until microtasks are run, so there's an opportunity to correct handle rejections created in this fashion.

Or, rephrased: by itself, Promise.reject() is not an unhandled rejection until the next tick.

ljharb commented 7 years ago

@chrisdickinson thanks for clarifying; that also means that throw foo in an async function also isn't an unhandled rejection until the next tick - which imo reinforces the argument that unhandled rejections are not uncaught exceptions, and thus should be under a separate option.

Jessidhia commented 7 years ago

If the unhandled rejection handler did not wait until microtasks run, it would actually be impossible to handle Promise.reject() or async functions that reject before the first await. Even if you wrote it as Promise.reject().catch(() => {}), by the time the .catch is called, the rejection has already happened. On Fri, Apr 21, 2017 at 7:48 Jordan Harband notifications@github.com wrote:

@chrisdickinson https://github.com/chrisdickinson thanks for clarifying; that also means that throw foo in an async function also isn't an unhandled rejection until the next tick - which imo reinforces the argument that unhandled rejections are not uncaught exceptions, and thus should be under a separate option.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/nodejs/CTC/issues/12#issuecomment-295951273, or mute the thread https://github.com/notifications/unsubscribe-auth/AAEdfSZzrzkDGABQyS-_YRtcnCxDy6MHks5rx-ClgaJpZM4JvaDI .

MadaraUchiha commented 7 years ago

I've been reading this thread for a while now, and I have to say, I hear the term "post-mortem" a lot. I'm a node user, I'd like to think I'm an accomplished one at that. I made my first OS contribution to node a few days (yey me!) and I've had to look that term up before I understood what it's all about.

I've never relied on core dumps to debug, most probably, none of the people I know (save for @benjamingr, probably) even know how to do it, let alone do it effectively.

I do use Promises though, often. I understand them pretty well, I see how they're useful over the callback model, and I often find myself wishing that the core APIs were Promise based and not callback based. I find them easier to use, easier to debug, easier to isolate in tests. I feel they make my programs more descriptive and easier to follow, which I value more than debuggability and even performance in many cases.

So I'm under the impression that the vast vast majority of node users would take a Promisified API over post-mortem abilities, even in the case those were to be hindered. That's doubly true since the feature in question is basically an opt-in, rather than an opt-out.

addaleax commented 7 years ago

So I'm under the impression that the vast vast majority of node users would take a Promisified API over post-mortem abilities, even in the case those were to be hindered.

While I agree a lot with what you are saying: I think the reason you’re not seeing much of people using Node’s post-mortem features is that it’s a different kind of user that is interested in them. A couple that have been brought up here are “Netflix, Pinterest, Uber, Twitter, Uber, Joyent, among others” – so, basically, large-ish companies that, when an unexpected error occurs, need to be able to know the exact state of the application in which that happened. Being able to get core dumps in that situation is a pretty important feature.

So, when you are saying “vast majority”, keep in mind that everybody has their own perspective and might not see that Node has a very, very broad set of users. :)

MadaraUchiha commented 7 years ago

Oh, no disagreements there. I'm just saying that in my opinion, the needs of those companies/individuals shouldn't come before the needs of the rest of us "regular" folks. Especially when the feature is opt-in, and not forced upon anyone.

addaleax commented 7 years ago

@misterdjules @yunong So, circling back to the issue with promises itself … basically, what you are mostly looking for is a way to get a coredump when a Promise handler throws or returns a rejected Promise, if and only if there is no explicit rejection handler present at that point. Is that correct?

So, for example:

Promise.reject(42).then(…); // does not crash, this is obviously intentional

Promise.resolve().then(() => {
  throw new Error(); // hard-crashes, there is a handler but it doesn’t catch rejections
}).then(result => result+1);

Promise.resolve().then(() => {
  throw new Error(); // does not throw, there is a catch handler
}).then(…).catch(e => …);

I think that’s possible to do, but would require V8 changes (and maybe spec changes) because the default 2nd argument for .then() is actually a re-throwing error handler, which is currently counted as a rejection handler for the second example above.

benjamingr commented 7 years ago

I think that’s possible to do,

I don't think it is, we had a lot of discussion over it back at the #5020 PR days and without crippling promises this is impossible. The problem is not really the second argument to then, it's the fact that rejections are deferred to prevent timing bugs in code ("zalgo").

Having async stacks (like C# has and not just async stack traces, like we currently have) would go a long way.

One "solution" would be to add a flag that when enabled - node preemptively take a core dump whenever a rejection happens (handled or not) and then if it is unhandled crash the process and use the core dump we took earlier. We can, for example, do it only for the first rejection of a type in a cluster so performance doesn't get too bad.

I'd also like to point out we currently don't have a good solution for post-mortem debugging in production with callbacks anyway (and outside of production you can just reproduce it and look at the stack/heap with the debugger). You can see https://github.com/nodejs/post-mortem/issues/18 on why - Node servers not using promises have to be very careful to not be susceptible to DoS as is - and if they add try/catch in places to avoid it, you still have to lose some core dumps to keep availability up. The "take a core dump first" solution is probably as viable as the solution suggestion in https://github.com/nodejs/post-mortem/issues/18 by @raynos.

Note that again, these post-mortem concerns are only pointing out something _problematic if the data is modified within a microtick and no I/O happens with the current hooks. Note that might change at some point.

Also note - that the ability to blacklist promises for post-mortem people remains exactly the same before and after this addition to the API.

Daniel15 commented 7 years ago

Cross-posting my comment from https://github.com/nodejs/node/pull/12442:

Are there any guidelines for what should go in the util module vs what should go elsewhere? it seems like there's a whole bunch of random stuff in util. I wonder whether it's worth making a separate cohesive module for async-related utilities (like promise-util or async-util or something).

benjamingr commented 7 years ago

@Daniel15 I think there is an extremely high barrier of entry to util now, it is very hard to add new features there and the need needs to be justified.

From what I've seen this is the criteria:

I think introducing a new module to Node should have a much higher barrier. There aren't many things in util right now (all the util.isFoo stuff is deprecated and not really used).

mikeal commented 7 years ago

I think we need a solution that gives users a version of the Node API that works with async/await but does not require us to touch the entire API piece by piece. I suggest we expose a util.awaitable method that takes a standard Node callback API and converts it to be consumable with async/await.

How is this an improvement over the "promisify" APIs already prominent in the ecosystem?

Responding to @benjamingr's points above:

I think that this thread has been fairly de-railed by the debugging concerns around Promises, which I don't think are relevant except in the sense that we may have much better support for Promises in the future that is dependent on native Promises.

I empathize with the feeling that we should do something but often doing nothing is better for our uses than doing the wrong thing or even something half-assed. This feels like a half measure, and I think it sort of is by design, which would be fine if we were much more confident about what better Promise support in Node.js was going to look like. If I could see our destination then I could say whether or not this is a stepping stone to get there, but I don't know what that destination looks like yet and that makes it a lot harder to get behind this.

P.S.

Some of these comments seem to dislike the error model Promises have. While that's a fine opinion to have it has become an incredibly tired argument against anything Promises related in Node.js Core. Core doesn't get to decide when/if people use Promises, they already are. It's the responsibility of Core to improve the experience of those users in Node.js. Blocking any improvements to that experience does nothing to slow down Promise adoption and only makes Node.js worse for a growing set of our users.

mikeal commented 7 years ago

Side question: There was an effort by @chrisdickinson to spec out what exposing a Promise API for core would look like. Chris is no longer driving that but it made it pretty far along, with a lot of thought put into the best way to expose a parallel API. Does someone want to pick up where he left off?

addaleax commented 7 years ago

The only case I can think of where core offers a unique opportunity to move past these penalties is by offering a native promise API, which we could do eventually but this would sort of conflict with it so this particular direction may be counter-productive.

That is actually one of the reasons why I like this approach – it allows core to provide custom implementations of the promise equivalents of callback methods instead of just wrapping them (see for example the timers changes in nodejs/node#12442, and that’s not even native code).

In general, it is not a good practice to include things in core with the express purpose of reducing dependence on ecosystem solutions.

I don’t see it as that, and I think many others don’t as well. I’d like to see us moving towards a world in which beginners learn to use Promise APIs before they learn the callback ones – that would reduce the entry barrier to learning Node in general a lot. (Actually, my engagement on this topic is substantially motivated by having that experience during NodeSchool and seeing over and over again how people try to wrap their head around callbacks – async/await is going to feel a lot more natural.)

So we're in this spot where this feature is either forcing a standardization around the native promise

Is that a bad thing? It might happen anyway because of async/await, and since Promise APIs are interoperable, I don’t feel uncomfortable doing this.

Does someone want to pick up where he left off?

Kinda? I would love to see that and I’d be willing to put a lot of work into it, but right now I wanted to take on a small-ish step forward and see if we can get Node core to commit to supporting Promises.

mikeal commented 7 years ago

So we're in this spot where this feature is either forcing a standardization around the native promise

Is that a bad thing? It might happen anyway because of async/await, and since Promise APIs are interoperable, I don’t feel uncomfortable doing this.

I think I didn't write this clearly enough. What I mean is, we're forcing people to standardize on the "natively implemented promise" over promise libraries implemented in userland like bluebird.

Like I said, we may want to do that anyway eventually, but at the moment bluebird is substantially faster than the native implementation and possibly more widely adopted by Promise users in the ecosystem.

I think we're in a tough spot. On the one hand, we could accommodate these users by allowing them to set their preferred Promise implementation, but if we do that we lose one of the ways we might force standardization in the future if we have native-only features. On the other hand, if we push people into the native implementation now we may be pre-maturely forcing that standardization without a lot of objective reasons for preferring native promises over userland.

Kinda? I would love to see that and I’d be willing to put a lot of work into it, but right now I wanted to take on a small-ish step forward and see if we can get Node core to commit to supporting Promises.

I empathize, and agree, with the feeling that we just haven't done enough.

After watching some of the earlier Promise discussions it was clear that there were a lot of options for what great Promise support would look like in Node.js. My hesitation in taking a small step is that it's not clear yet what direction we want to go in as a final destination, and so it is hard for me to tell if this specific step moves us in the direction we want to end up in for the best Promise support.

That is actually one of the reasons why I like this approach – it allows core to provide custom implementations of the promise equivalents of callback methods instead of just wrapping them (see for example the timers changes in nodejs/node#12442, and that’s not even native code).

Interesting. I can see that I was wrong in assuming that this approach would limit our perf options. I still might be concerned that maintaining this alongside parallel API might be a maintenance burden but I think we can probably find a clever solution to that.

ljharb commented 7 years ago

async/await already forces people to standardize on the natively implemented promise - whether node ships a util function or not, "use the native Promise most places" is the new reality, for bluebird users as well as everyone else.

kyrylkov commented 7 years ago

@mikeal

but at the moment bluebird is substantially faster

How do you define substantially?

mikeal commented 7 years ago

@kyrylkov theses were the last benchmarks that I saw. https://blog.wikimedia.org/2017/02/17/node-6-wikimedia/

TBH, I think that this overhead is quite minimal in terms of IO delay, but is quite a lot for a low level primitive.

styfle commented 7 years ago

@mikeal That blog post looks like a typo in either the node version of the V8 version.

the V8 team has made some inroads in Node 7.5.0 (with V8 5.3), but is still trailing Bluebird significantly

However the downloads page shows that Node 7.5.0 is using V8 5.4.

mcollina commented 7 years ago

In some experiments I have done with v8 5.9 (I+TF) I saw no real difference in performance between callbacks and native promises. It was not really scientific and reproducible, so a more formal approach is needed.

benjamingr commented 7 years ago

@mikael I'll later respond to the rest of the feedback, but as bluebird core I have to say:

In addition to being impossible in JS userland - promisify also enables our APIs to be uniformly consumed by async/await and lets other callback APIs do so cleanly (the custom parameter).

I also think I may have expressed myself poorly in terms of the dependency so apologies for that. It's about having a standardized way to consume callback APIs with a uniform way for them to declare how. I think this would help our users a lot.

mikeal commented 7 years ago

Native promises are as fast as bluebird in recent V8. They really did a lot of good work on them.

:fireworks: Do you have a link that shows this in latest V8 somewhere? I'd like to get this out more, even get some tweets about it from @nodejs on twitter :)

mikeal commented 7 years ago

After the responses as well as a few other individual conversations I want to say that all of my concerns have been addresses and this LGTM :)