-
Notifications
You must be signed in to change notification settings - Fork 11
Default Unhandled Rejection Behavior #26
Comments
/cc @chrisdickinson You've been on the front lines here. Would like your summarized opinion and any conclusions. |
This is reasonable, the current behavior is really confusing - especially to people who are either new to promises or are callback users that are interacting with a promise returning library |
Ugh do I really need to make my argument here rather than a PR with actual code? Fine, give me a couple days or something. |
Baring the post-mortem issue (which remains unfixed) and potential shared state cleanup issues, I think this is indeed the biggest problem users have with promises. They start using them, and when the first thrown error occurs there is absolutely no feedback whatsoever as to what happened. Browsers can add all sorts of fancy mechanisms to log (and unlog) errors and keep track of currently pending promises, unhandled rejections and so on. Unfortunately by default node only has stdout and stderr. So yes, I think the most sensible thing to do by default is to at least log a warning, but let expert users and monitoring tools add their custom Theres been a split in the promise community on this issue, with people advocating any of
Since we don't have UI and don't have built in |
If you'd like to pursue a PR for adding the default handler that logs by all means go for it and I'll gladly help review it. |
Ok never mind, I guess I feel strongly enough about it to write it now.
Agreed, this isn't even a solution; we should do something.
These 3 seem to conflate two things: a user-hookable event ( |
There's also a thing here about explicitness -- many of do not find ignoring errors a light thing. We wanted to have a flag to have unhandled errors (any kind) only stop the call stack and warn, without crashing the process, that may be viable, if it is really what you wish for. |
Logging is better than not doing anything and backwards compatible |
@Fishrock123 thanks. Some clarifications: As far as I'm aware the decision to abort on all errors was partially made because errors leave node in an indeterminate state. This is not the case with promise errors generally - if a promise rejects the process generally keeps functioning correctly. This is not the case with unhandled rejections though - those imply a promise chain has been broken and no one was there to recover from an error and those can definitely leave the process in indeterminate state. The problem is Code can be written in a way that does not cause an intermediate Also - it's not like logging does not have a precedent - we have a whole warning infrastructure for things that are deprecated, or are possible programming mistakes (like event emitters having too many handlers). Between logging and doing nothing - I'd go for logging. As for GC - it suffers from false negatives the same way the current approach suffers from false positives. I think relying on observable GC to perform something drastic like abort the process is extremely slippery slope. I'd like to (Try to) summon some people, should they wish to participate @erights , @domenic , @kriskowal to represent the counter-stance. |
There is nothing wrong with having a rejected promise with no handler, that 10 minutes later receives a rejection handler - that's why browsers have hooks to "un-warn" about unhandled rejections. Exiting the process by default is a non-starter imo because that conflicts with the actual language semantics and expected uses. |
@ljharb well, yes theoretically there is nothing wrong with it if it does get handled 10 minutes later. The problem is that it's impossible to determine ever when the error gets handled - it's easily reducable to the halting problem It's entirely within the power of the node CTC to decide that in node programs such promise usage (resolving after 10 seconds) would cause the process to abort by default (with an opt-out). It's definitely an opinionated take (which is why I pinged domenic, mark and kris) who IIRC object to it. I'm +1 on logging by the way since it's easy and has an easy opt out. Saying something conflicts with actual language semantics and deciding what expected usage is for Node will likely not yield productive discussion - some members of the node CTC feel ignored by TC39 anyway - so I suggest we focus on problems (promises resolving after 10m) rather than telling Node what it can or cannot do. |
Not sure if this is worthwhile discussing; it seems to depend on one's definition of "indeterminate state". I think it is probably good to err on the side of safety, people may not build robust programs.
(Also re: @ljharb)
In theory; logging to warn is currently very inconsistent, but hopefully that will improve with Jame's warnings PR.
Could you define "false-negative" here? Let me clarify my plan (which I think I can get working..):
|
var settings = file.readAsync("nonexisting.json").then(JSON.parse); // let's say the file doesn't exist
// rest of the code here Since this is in global module scope - it would probably never be garbage collected. |
@benjamingr right, I experienced it and thought about it, which is why I'm going to do my best to ensure my [2] is possible. :) |
@benjamingr fwiw, in that case i was speaking as a JS developer - I would be very displeased if my node process exited because I intentionally deferred handling of an unhandled rejection. @Fishrock123 totally agree that if it exits on GC there's no conflict with my expectations, and it'd be a good time to exit by default. |
Catching up now — my opinion has pretty much come full circle since the start of this conversation. The TL;DR is that I think that @Fishrock123 is on the right track by making Node crash if an unhandled rejection is garbage collected. To expand on this: it's tempting to say we should take some default action on unhandled rejection and that programs can be universally written to avoid hitting the unhandled rejection handler. It's especially tempting because it looks like it gives @nodejs/post-mortem an out — in that "promise rejects, is unhandled, we crash", at first glance, looks like it lines up with what they need. However, we always need to wait until next tick to fire the unhandled rejection handler, because doing otherwise causes promises to exhibit confusing behavior from users' perspective — we always want to give synchronous rejections a chance to be handled on the same tick before firing unhandled rejection. This means that we always have to wait until next tick, so crashing on unhandled rejection does not help post-mortem users. Crashing the program on unhandledRejection in the absence of a user-installed listener, as mentioned by others, introduces an artificial difference between Node Promises and browser Promises — it invalidates patterns of use that are valid in browser. This is not great for promise users, and should be avoided if we can help it. Logging is a weird one — it seems like the most innocuous, but consider that many applications use JSON-based logging, like My recommendation would be to crash on unhandled rejection GC, per @Fishrock123's plan. I'm not sure about crashing the program on exit if there are unhandled rejections, since it may be that folks are eagerly replacing a rejected promise — we'd have to investigate that. WHATWG streams may do this, IIRC. With regards to @benjamingr's false negative example — if that were run at global scope it would never crash the program. Running it at module scope, however, would crash at some point after the module has been run. You could get it to exhibit false-negative behavior from a module by exporting the promise, requiring the module, and never handling the promise, but that seems like it would be a fairly uncommon case. |
Node (and v8) already writes to stderr for plenty of reasons regardless of whether you are using JSON-based logging or not. And some of those cases are not even configurable. Having something that you can configure only default to stderr must therefore not be as big a problem as you make it out to be. I am not sure if everyone realizes how the GC works but it is not reliable and it doesn't guarantee that an object is ever collected. I don't consider this acceptable unless you also add something that reports all unhandled rejections on process exit so that there is some guarantee. |
@petkaantonov I quite agree.
If there is something that would cause this to report "false positives", I'm not really sure. I'm beginning to think that promises necessitate JS vm's to do reference counting so that we can properly detect when something goes out of scope. :( |
If a program exits with an unhandled rejection that would have been handled had that program exited only a bit later, is it really a handled rejection? I think it depends on arbitrary philosophy. I personally just avoid the headaches of that by a simple work around: promise.catch(noop);
setTimeout(() => {
promise.then(function() {
});
}, 5000); If |
Update: so far I'm unable to get WeakCallbacks on promises to fire in a situation that doesn't use |
Here's a test case I wrote up as a native module to demonstrate that rejected handles don't trigger the weak callback: nodejs/node#5292 (comment) |
As a node user (not a contributor) who's been using async/await (via babel) for about 6 months, I am a big proponent of node crashing on unhandled promise rejection but I have no strong opinions on the mechanism. This would match the current behavior of an uncaught error, which feels like the semantic equivalent of not catching a rejected promise. Just like any other error, a rejected promise would need a I think backwards compatibility should not be an important factor here - major version releases are for (useful) breaking changes. This is a very worthwhile breaking change, imo. Plenty of warning via communication and documentation will help prepare users. |
Please,please,please do not change the existing behavior of the current native Promises. Letting rejections be swallowed is a feature- not a bug. I have a TON of production code that leverages this intentionally. Think: Fire & Forget code... there is a genuine purpose for it. YES- I have missed a
IMHO, These complaints seem equivalent to the complaints about |
Is a solution that is exactly like the browsers (specifically: make hooks available for warn and un-warn, and when those hooks aren't used, warn and un-warn by writing to stderr) not viable for some reason? i think exiting the process would be a little too extreme given @ljharb's comment about intended semantics of Promise. there are situations where not handling is exactly what a programmer may have intended because she knows how she wants to handle it later. that technique should not be "taxed" by having to structure the handlers in some specific way in node. if you want control of the stderr output (such as needing it to be JSON formatted), attach the optional handlers and do what you want with the error. i would rather pay that "tax" than losing on explicit warnings as my default, or forcing the techniques such as above to be ruled out and superficially make my JavaScript not portable to node/browser runtimes. |
If you known a way of clearly letting users know that they have an error that won't impossibly get lost in logs that works cross platform, other than exiting, please let me know. I don't know of such a thing. |
@Fishrock123 I haven't tested this, but is this what you're looking for?: loud-rejection. |
I would like to progress this discussion. Current behavior is really confusing for Node and Promise users. My first impression, I just add flags option like Here is the example(currently i have not written I don't read the full discussion in this thread. So my opinion may be irrelevant, but I would like to settle down this discussion until v6 released. IMHO, if code throws |
@yosuke-furukawa you can change the default behavior by doing: process.on("unhandledRejection", (e) => {
// handle error
}); Although personally I'm strongly against not having a saner default like logging. |
Yes. But I don't want to add the
Agree, but saner default definition is quite difficult. everyone has different context. so I would like to add flags and not change default behavior. |
This would make Node and JS in Node context way more powerful than Nashorn and other pure engines and put it side by side with Akka and OTP usage. Freeing the Event Loop from this responsibility is almost a natural choice. Maybe we'll have to rethink errors (and I don't really see a hard change on this). But at least, I think community should consider exploring more the libuv capabilities or we will have a halved awesome potential forever. Promises should be streamed and async await should hide a streamed resolution. This is not my idea... C# and others do it. Please, please, please think in it. |
I invite you also to check out the es-discuss mailing list. It's the best https://esdiscuss.org/ On Wed, Nov 2, 2016, 08:02 Leonardo Dutra [email protected] wrote:
|
And again, there are efforts to parallelize JS where this sort of input might be appropriate although to be completely fair I would probably try to approach those bodies with a less argumentative tone. This is about bettering the life quality of developers - let's stay positive. I'll repeat my recommendation for esdiscuss.
Async processing does run in a libuv threadpool and notifications are passed to node through event listeners. One of the more common misconceptions is that node is single threaded - that is certainly not the case - node just has a single JS thread. In addition, the
While I appreciate both OTP and Akka I find them better than node for some things and a lot worse for others. They use a paradigms and put the emphasis in different places. Node is, like most software a result of many tradeoffs. For a lot of what I do it's fantastic, for a lot I wouldn't consider it.
Consider looking at the As for errors - I definitely think we need to rethink errors. Easier said than done :)
I'm not sure what that means but I'm very intimate with how async/await is implemented in C# and it's pretty similar (sans things like synchronization context, and C#.next efforts to replace GetAwaiter with something nicer). There are two proposals for streaming at the moment - async iterators and observables. Just to repeat, this is not the place to discuss these things - esdiscuss and GH issues on https://github.com/tc39/proposal-observable and https://github.com/tc39/proposal-async-iteration are. It sounds like you sound passionate about this - by all means there are a lot of ways to contribute to Node, JavaScript and the ecosystem. Should you ever find yourself "stuck" ping me - there is a lot more work to go around than people able to spend time and do it. On a less positive note - whenever you comment here it pings 22 people via email. So this is my last reply here that's not directly about the default. |
Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
Refs: nodejs#12010 Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 PR-URL: nodejs#6375
src: use std::map for the promise reject map Refs: nodejs#5292 Refs: nodejs/promises#26 Refs: nodejs#6355 Refs: nodejs#6375
@benjamingr - Does anyone have an update on this? I read in nodejs/node#12734 that the new decision is to exit Node when an unhandled Promise rejection occurs and there is no I agree with this behavior (long debate on nodejs/node#830), and I think that crashing when a rejected Promise is garbage collected is... well... sloppy... especially when you're just trying to make <1% of Promise users (those employing "late" catch handlers) happy. It introduces the possibility of false negatives, and it may cause programs to fail more often when under memory pressure. As mentioned numerous times, users could simply: process.on("unhandledRejection", () => {}); to override default behavior and add catch handlers late. Just like you can do this if you want to live on the edge: process.on("uncaughtException", () => {}); IMHO, I think it's very important for asynchronous and synchronous uncaught errors to have the same default behavior. Currently, I just do this at the top of my process.on("unhandledRejection", (reason) => {throw reason;}); I don't care if I need to continue that practice or not, but I certainly have my opinions formulated about unhandled Promise rejections. What's a tad frustrating for me as a Node user is that this debate has been going on for 3+ years. :( I feel like we have all expressed our opinions, and all of the facts are out there. That said... what is the final consensus? Any idea when the decision will impact Node 8 or Node 9 users? |
fail early is a good and consistent concept. i don’t see why async stuff should be treated as some kind of low priority background stuff that can be allowed to fail. |
Because the language explicitly is designed to, and does, allow late failures (#26 (comment)) |
@ljharb - We've each argued our sides. I can see the use case for late catch handlers, so I understand your frustration. Still, please keep in mind that we are only discussing default behavior; no one is saying you can't have late catch bindings. After all, it's one line of code to prevent the default behavior, and more complex logic could be split into a 3rd party module. I'd be happy to argue further, but let's take it offline -- just email me. At this point, I'd just like someone to make a decision and stick with it. :) It's pretty clear what I'd prefer, but I'll accept whatever is best for the Node community as a whole. |
@bminer let state = 'pending'
const promise = new Promise((resolve, reject) => {
// this executor runs before anything else,
// and since it is synchronous, the promise-state
// is rejected before we can attach the handler to it below:
state = 'rejected'
reject(new Error('uh-oh'))
})
console.log(state) // 'rejected'
promise.catch(e => { // technically attached "late"
console.error('my handler:', e)
}) This is because Promises are not true monads: instead of "lazy-loading" them (when a handler is attached via This is why I like the GC approach. |
@jrop that's not technically attached late in Node's regard, attached late means I/O already happened: var p = Promise.reject(new Error());
p.catch(() => {}); // not late
for(var i = 0; i < 1e9; i++); // busy loop
p.catch(() => {}); // still not late
process.nextTick(() => {
p.catch(() => {}); // still not late
});
Promise.resolve().then(() => {
return new Promise(resolve => process.nextTick(resolve));
}).then(() => {
p.catch(() => {}); // still not late
});
fs.readFile("foo.txt", (err, data) => {
// this is late
}); |
Also:
Promises are not "executed", promises are the results of an already existing action. Monads have nothing to do with laziness (haskell thunk laziness does, but that's orthogonal). Monads are just things that implement the monad interface (belong to the monad typeclass). The requirements are having an unary return (Promise.resolve), a type constructor (Promise) and a bind operator (Promise.then) with the signature |
@jrop And to add to @benjamingr's comment, what makes Promises not monadic is that you can't nest resolved ones. Specifically, the monadic laws require that |
Actually the laws don't hold even if you forbid nesting
Immediately this is not true, since But lets say we restrict the type of This breaks application of the law in equational reasoning. If we write Equational reasoning is the main reason that the laws exist, so I believe the above is a sufficient reason to say that the law doesn't hold, and therefore a Promise (even the variant that would allow nesting) is not a monad. In fact, its very difficult to uphold any sort of equational reasoning if there are any eagerly executed side effects present, since the order of execution is defined by the order of evaluation! |
I vote for another async hook alternative, similar to Considering async/await, the stream is declared async and run in a async way. Their starter should be able to try {} catch(err){} if necessary (as it is). And the process, as a "container" of operations, should be notified of it's internal events (of any kind). Beautiful? Maybe not for some practical eyes... but essentially aligned with general concept of processes, events and "green threads". I still think Node.js should have a thread runner lib, using Promises/events for communication (like service workers or common Java/Go threads)... but this is another discussion. Currently, It's weird to think in a Node.js Actors model without it. And if there's someone trying to implement this way, I don't really wanna see. 🤢 +: I think logging unhandled on stderr is very valid... making possible to flag for no logging. |
Update: Agreed course of action:
The agreed on policy is:
This should please everyone involved since all use cases are supported and we don't introduce abortive semantics unless we're 100% sure the promise is unhandled. Libraries can even decide to opt out of the default behavior.
Continuation of nodejs/node#830
After a few months of
process.on("unhandledRejection"
event I'm convinced we need a saner default.Just to be clear - this is about the default behaviour of a rejected promise with no
catch
handler (orthen
handler with a function second argument) attached. Basically - anything that firesThe different approaches
process.on("uncaughtException"
to deal with. - this seems impractical now since the hook has been live in the ecosystem for a while. A flag might solve this.Proposal
When an
unhandledRejection
event is fired if no handlers are attached the error is logged to stderr. The current behavior is confusing to users and makes it appear as if promises swallow errors. While theunhandledRejection
hook has been used a lot in the wild a lot of users (even experienced node users) can forget it and get surprising behavior.People will still be able to opt-out of it by installing any
"unhandledRejection"
handler.This aligns Node's behavior with browsers and other environments as well as userland solutions.
The text was updated successfully, but these errors were encountered: