-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lib,src: eagerly exit process on unhanded promise rejections #12734
lib,src: eagerly exit process on unhanded promise rejections #12734
Conversation
Refs: nodejs#12010 Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
|
||
Local<Promise> promise = args[0].As<Promise>(); | ||
|
||
CHECK(promise->State() == Promise::PromiseState::kRejected); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tiny nit: CHECK_EQ? :)
void InternalFatalException(v8::Isolate* isolate, | ||
v8::Local<v8::Value> error, | ||
v8::Local<v8::Message> message, | ||
bool from_promise); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn’t used outside of node.cc, right?
gc(); | ||
gc(); | ||
gc(); | ||
/* eslint-enable no-undef */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as in the other PR(s?): Do you need the linter comments if you use global.gc()
instead?
I think I would like that, yes. It doesn’t seem like it would be hard to edit this PR for that? |
I'm liking this approach better but I'll have to dig through the details a bit later. In general, +1 on this tho. |
I don't want to get re-involved in this whole promises-in-node thing very much, but I hope I can drop some words here and maybe influence some people who will continue this discussion. I am very against this, as it misunderstands the idea that promise exception handling can happen asynchronously. There is no good analogy here with sync exception handling. Of course it's reasonable to abort the process if a sync exception isn't handled synchronously; that's the only way it could be handled. But it's not reasonable to abort the process if an async exception isn't handled synchronously. Collection on GC seemed like a great way to go for me, since that's when you actually know something is not handled. The objections against it are not very strong, IMO. You can make it opt-in if there's a performance concern. The GC thing doesn't work for other promise libraries, but neither does this PR, so I don't understand that argument at all. (It just provides a utility function for them, but that can be done in both cases.) The last thing I want to draw attention to is that this essentially breaks any attempts at doing parallelism with async/await, as illustrated in #12010 (comment). This seems pretty terrible to me, as that coding pattern is a great one that people will expect to work across all environments and will use increasingly as async/await gains ground. I think this crash thing might be fine as an opt-in, so people who really want it can use it instead of installing an npm package that gives you the right hook. But not as a default; it just breaks the model of promises too much. |
I agree with @domenic, exiting because a handler wasn't added synchronously doesn't really make sense, there's lots of times you want to handle asynchronously for example: const request = require('request-promise')
async function concurrentRequest(urls, concurrency=5) {
// Start of the first lot of requests
// we don't care about errors yet
const urlQueue = [...urls]
const promises = urlQueue.splice(0, concurrency).map(request)
const results = []
for (const url of urls) {
try {
// wait for a promise, now is when we actually care if it failed
// or not
const data = await promises.shift()
results.push({
url,
data,
isError: false
})
} catch (error) {
results.push({
url,
error,
isError: true
})
}
// Put another promise on the queue
if (urlQueue.length) {
promises.push(request(urlQueue.shift()))
}
}
return results
} However with this change I'd need to add empty const request = require('request-promise')
const pointlessNoopOnlyForNode = _ => {}
async function concurrentRequest(urls, concurrency=5) {
// Start of the first lot of requests
// we don't care about errors yet
const urlQueue = [...urls]
const promises = urlQueue.splice(0, concurrency).map(request)
.map(promise => promise.catch(pointlessNoopOnlyForNode))
const results = []
for (const url of urls) {
try {
// wait for a promise, now is when we actually care if it failed
// or not
const data = await promises.shift()
results.push({
url,
data,
isError: false
})
} catch (error) {
results.push({
url,
error,
isError: true
})
}
// Put another promise on the queue
if (urlQueue.length) {
promises.push(
request(urlQueue.shift())
.catch(pointlessNoopOnlyForNode)
)
}
}
return results
} And personally I think it's even worse than that, by adding at a Another point I'd like to make is that checking synchronously after Promise rejection happens makes no sense because it means someone could still add a handler asynchronously as long as they do it before the rejection happens. For example this will fail only ~50% of the time whereas based on this solution I'd expect it to fail 100% of the time, essentially all this behavior does is add bizarre race conditions that aren't intuitive, predictable or useful. function delay(time) {
return new Promise(resolve => setTimeout(resolve, time))
}
async function example() {
const p = new Promise((_, reject) => {
setTimeout(reject, Math.random()*2000)
})
await delay(1000)
// This will definitely cause people bugs given that sometimes asynchronous handling will work just
// perfectly fine, while this isn't a real example this sort've race condition will definitely happen in
// real code
p.catch(_ => {})
console.log("Reached")
}
example() |
This will break the ecosystem. Unhandled rejections are normal and should not exit the process. |
@ljharb note that this is only on GC of an unhandled rejection. |
@benjamingr That is the case for #12010, this PR is the nextTick one ;) |
@addaleax Oh, then I agree with @domenic, exiting by default on unhandled rejections is not the way to go, although admittedly not doing so would be worse for post-mortem reasons. I think that we should give the ecosystem some time to stabilize now that async/await is out, focus on making our own APIs work with async/await (like If people complain a lot about concurrency, we might even have to relax detection - but I assume we won't. The only positive thing about this PR is that users can easily opt out of it with a handler added. It would probably work for my code base (I don't buy the particular examples above since no one uses |
@benjamingr Can you explain what you mean when you say it would be good for post-mortem reasons? Because in the case of GC you definitely know the value wasn't used. Regardless of if you use However because with Promises you can listen to a value anytime you want, and this is often useful, while my final example in the above post isn't realistic in The first example I provided is mostly real, it's a simplified version of a piece of code from a scraper I wrote which is perfectly sound and reasonable, yet this pull request would make that not so, sure I can just add The thing is Promises (and async functions) aren't just about turning callbacks into synchronous looking code despite people often saying "Now you can write async code like sync code" (I'm not saying anyone here is saying that but there's certainly a mindset I've seen arising of it), if that were true then it'd be safe to actually block for real on a Promise, within some async functions that might actually be fine, however for the most part you probably want to use them in an environment where lots of tasks are running concurrently, in such cases it's important that consumers of Promises can decide to consume them when they're ready. The worst bit for me about this pull request is that I actually thought the Deprecation warning was always referring to garbage collection given that the error message states |
Good question, and sure. When you exit after a microtick (rather than wait for GC) then I/O has no chance to come in and "corrupt" the heap. When you wait for GC, an arbitrary amount of modifications can happen in the meantime - so the core dump you might get at the end is a lot less useful. That said, I am not in favor of breaking the abstraction in order to do this. |
Also, I've been meaning to suggest changing the deprecation error myself, the naming of it is very poor, "unhandled rejections" aren't "deprecated". This is my mistake twice, once for not arguing for a better message when the error was introduced in 6.6 and one for not fixing it yet even though @domenic has pointed out it's a problematic message (which I agree with). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really LGTM.
@benjamingr So are you saying your position has changed since #12010 (comment)? (It's fine if it has)... or else I am misunderstanding something. |
This is the approach I'd like to see Node take with regards to promises. Every time I've had an unhandled rejection log in one of our services, it represented a programmer error. The service ends up in a bad state — my desire would be for Node to crash (within a tick) with a log of the reason so that It is always possible to write code using promises such that it doesn't trip the unhandled rejection handler, even in async functions of high concurrency. Either the author can explicitly mark the promise as handled, by creating a no-op derived promise via |
However, that's not a step that is required in the JavaScript language, nor in browsers - so this is asking everybody who wants to write interoperable code - even non-node users - to complete extra steps. This will also mean that code written with the browser in mind that already works in node, might suddenly not work in node without modification. Please don't undersell the interoperability and maintenance burden that this change will impose, even if node ends up deciding to do it. |
@jasnell To further explain my point that I mentioned earlier: Let's label noticing an unhandled rejection on next tick as I think that, in the current situation:
I am aware of the concerns about delaying to
That said, I see an alternative path forward, given the current actions on the Chromium side.An alternative way would be to somehow change the spec to directly forbid that behaviour.
This would perhaps be the better choice for the Node.js core than the first one (less logic, less changes, less flags, better debugging), but it requires a spec change. /cc @ljharb, @domenic , perhaps? Shortly put: I think that violating the spec is worse than all the other concerns against delaying to I have no idea atm how exactly should that spec change look like to cover both the browsers and Node.js behaviour without an UB there. |
@ChALkeR well written. I think a problematic point is that Note that in practice, I've done "exit on tick" and no library ever broke because of it, I've found it to be the case that no libraries "in the wild" really use this capability very much, and the few who do opt out explicitly (bluebird has a That said, I think the real path forward is spec change, and introducing synchronization contexts on top of
Alternatively, allowing the user to set the promise scheduler solves all this too, but gives users a lot of power I'm not sure we should give them (this can be done at the platform level, without spec changes). Splitting the app into zones this way allows for clear "context"s the library/framework defines for when something won't be handled. Some examples:
This is a little bit like domains, but without the domain problems with error recovery and handling - since we're not recovering from errors - just detecting errors people forgot to listen to. |
@benjamingr ... while I have many concerns around zones, at an abstract level what you describe is certainly ideal... that is, the idea that some additional spec changes that provide clear hook points for all of this would be absolutely fantastic... and that is something that I can bring back to TC-39 to discuss. It would be excellent. At this point, I think there are still way too many open questions and potential issues for us to reasonably come up with a reasonable default behavior. I think we need to take a more iterative approach. At the collaborator summit we talked about a flag driven option that would allow users to opt in. It would also allow us to experiment with various options to see what is the best strategy. So here's what I would recommend: We add a I know that this is not an ideal solution because it kicks the ball down the road a bit, and adding multi-valued command line arguments is always irritating and causes it's own set of problems, but going with this approach would allow us collectively to experiment over a longer period of time with a range of possible strategies. It also gives us time to go back to TC-39 or the VM implementors to work through possible spec modifications that can provide a significantly better solution. |
Huh – just to be clear, the current behavior is to warn on nextTick. And if we want to leave the option of either warning or crashing on nextTick open, then we should keep that, because it’s much easier to move the warning/crash to a later point in time than to an earlier one. |
oh.. right sorry, I get those mixed up. Yes that, keep the current default but allow a flag to enable the other behaviors |
@jasnell just do add one note: we also discussed about printing the stack trace of the offending exception as well. I would prefer to do so for every |
The stack trace of the offending exception is already printed if |
@jasnell I'm not sure what a command line switch would get us - users can define an The default behavior gave users a bad experience before warnings were added - I think they really helped. I also think that I think adding a flag won't really solve much - I think Node should lead the push for solving scheduling and unhandled promise rejections with technology - I'll hopefully set up a prototype next month (super busy :( ) that:
|
What the command line switch does is provide a more opinionated view of what the options are. Yes users can handle |
I think providing multiple options is a recipe for confusion - saying "we're not sure what the right thing to do is, here are some options" in core is something I'd rather avoid. We're aware of better alternatives for the culture, and what we currently do works pretty well and solves the hardest problem (swallowing rejections) already for us. I'm definitely in favor of throwing on GC and keeping the warnings on tick in place. I think it's good middle ground until we build synchronization contexts or schedulers to address this (what other platforms do). |
That would print the stacktrace of the warning, not the original stacktrace that caused the warning. I propose to print the stacktrace of every error that reaches the More or less, make this the default behavior, while maintaining the warning: process.on('unhandledRejection', function (err) {
console.log(err.stack);
})
Promise.reject(new Error('kaboom')) |
@@ -673,6 +673,8 @@ asyncTest('Throwing an error inside a rejectionHandled handler goes to' + | |||
tearDownException(); | |||
done(); | |||
}); | |||
// Prevent fatal unhandled error. | |||
process.on('unhandledRejection', common.noop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: common.mustCall()
instead of common.noop
?
@mcollina yes, that makes sense |
I think discussion in the last CTC meeting was that we should pull together the group of interested people to start working on post-mortem/promises. I think @jasnell took the action to push that forward. I'm available to help with that at well if necessary. |
I would definitely be interested in being a member of such a group @jasnell |
Does anyone know the current status of this (and #12010)? Is this going to be taken in at some point? Situation is that by not having that, ecosystem is really broken. Just recently I approached a popular project (19k+ stars, 1.9k+ forks, 100+ contributors) which had failing tests in master and nobody knew about it. Purely because unhandled rejections are not exposed as exceptions. |
There is a newer one by @BridgeAR that's fairly recent #15126 (comment) Contribution and involvement are welcome |
@Fishrock123 I am closing this for now. I am going to proceed with my PR soon. If you would like to continue working on this, I would very much appreciate that as well (especially as C++ is hard for me - I learn it while working on the code). |
This PR is an alternate form of #12010, which takes a different approach to providing a good default for unhandled promise rejections.
This makes unhandled promise rejections exit the process though the usual exit handler if a promise is not handled by the time the
'unhandledRejection'
event occurs AND no event handler existed for'unhandledRejection'
.This is as per the current deprecation message.
This no longer waits, or attempts to wait for GC. The reasons for which may be summed up in more detail in @chrisdickinson's lengthy post in the previous thread.
The change is also partially due to GC handlers having potential issues with emitting the process
'exit'
event back to JavaScript for regular exception cleanup. This approach avoids that problem.In addition, this publicly exposes (docs & naming pending)
process.promiseFatal()
, allowing custom promise implementations to implement identical behavior when a promise should exit the process.Edit: as a note I am not opposed to keeping the warnings behavior as an option, e.g. with a flag.
Refs: #12010
Refs: #5292
Refs: nodejs/promises#26
Refs: #6355
PR-URL: #6375
Once again, cc @nodejs/ctc, @chrisdickinson, & @benjamingr.
CI: https://ci.nodejs.org/job/node-test-pull-request/7738/
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
lib, src, promise