Repository navigation
[Discuss] Resolving the unhandled rejections issue #22822
Description
@nodejs/tsc @nodejs/collaborators (yes, pinging @nodejs/collaborators is intentional and yes, I know it causes lots of people to get notifications... that's what I want because I want broad input into this conversation)
We currently have two PRs open that deal with unhandled promise rejections.
Both do or assume several things:
- Both introduce APIs that users may use to observe
Promisestate. - Both make assumptions (albeit very different assumptions) about what the proper handling of unhandled rejections by Node.js should be.
These PRs have been generally treated as mutually exclusive by the authors and the discussion about the introduced APIs has been generally held up by disagreements in (a) the fundamental handling of unhandled rejections and (b) whether or not Node.js should warn users about potentially problematic coding patterns. Unfortunately, what I see happening now is that issues that should be separate threads of discussion are being lumped all in together. So I'd like to try to separate these things out.
Warn or Not Warn
Let's take the whole discussion about Promises out of the picture for now. There is a general philosophical/values discussion to be had about whether or not it is Node.js' responsibility to warn users about potentially problematic coding patterns that may exist. It has been argued by a couple of folks that it is not Node.js' place to "judge" someones code. This has been expanded to mean that when the user runs some code that is not ideal but otherwise runs, Node.js should just stay silent on the matter.
An example of this is resolving a Promise twice... e.g.
new Promise((resolve, reject) => {
resolve('A')
resolve('B')
});
or
new Promise((resolve, reject) => {
reject(new Error('A'));
resolve('B');
});In both cases, the resolve('B') is extraneous and will be swallowed by default by V8. The code will run but the second resolve is effectively a non-op. In a handful of cases, this situation could be intentional or could be a legitimate coding bug. Take the following, for instance... a forgotten break statement leads to the swallowed resolve:
let foo = getFoo()
new Promise((resolve, reject) => {
switch (foo) {
case 'A': resolve('A')
case 'B': resolve('B')
}
});When we look at this, we can likely reason that this is clearly a programmer error, but the code will run just fine.
Thanks to an internal callback API provided to us by V8, Node.js can detect when this secondary resolve or rejection occurs. The question is, should Node.js, by default:
- Emit a process warning letting the user know that this situation occurred, or
- Invoke an optional callback API that the user must explicitly opt in to using to be notified about this situation
- Both
Note that when the process.emitWarning() API was added back in the Node.js 6.0, I took the approach of allowing both. By default, process warnings are emitted to stderr. Usercode can, however, listen to the process.on('warning') event and use the --no-warnings command-line flag to disable the default handling and provide their own handling of the process warning events. Because runtime deprecations now use the process warning mechanism, runtime deprecation warnings can be handled in the same way.
For warnings about synchronous file system I/O, however, we take a different approach. Synchronous I/O can be a significant performance bottleneck in Node.js and is generally considered to be an antipattern, but there are perfectly legitimate use cases for it. Therefore, we do nothing by default when sync I/O is used but we provide an opt in command line flag --trace-sync-io that users can switch on to help identify where sync io may be happening within their applications.
Given precedent, I don't think we can argue that there is any single rule of thumb to be applied here. Whether or not we warn by default can really only be determined on a case by case basis determined by how useful, or how problematic, a given coding pattern may be. So with that, we need to determine if swallowed resolves/rejections are potentially problematic enough to warrant a warning by default.
Crash or Not Crash
When a Promise is rejected without a catch handler in place, what do we do? This has been a long raging discussion for several years now without any clear resolution. Some feel we should crash immediately, some feel we should ignore and just let it happen. Neither are wrong, neither are entirely correct. I would submit that this is something that Node.js should allow users to configure with a command line argument, but then the argument turns to what should a reasonable default among several options be. That's what we need to figure out here. For my money, crashing on garbage collection is not ideal but it's likely the best reasonable default option we have at this time so long as we also have the option to crash immediately or not at all available for users to select.
One API to Rule Them All
The two PRS currently open propose two different APIs users may use to get notified about Promise state.
The "swallowed resolves" PR (#22218) takes the approach of building on the existing process.on('unhandledRejection') event and adds a new process.on('multipleResolves') event.
For example:
function handler(type, p, reason, message) {
// do something here
}
process.on('multipleResolves', handler);
new Promise((resolve, reject) => {
resolve('A');
resolve('B');
reject(new Error('C'));
});The "promise events API" PR (#21857) takes a more diagnostics API approach that adds a low level API for monitoring promise state. The basic idea would be to replace the existing process.on('unhandledRejection') event and to allow users to implement whatever handling they want:
const makeHook = (type) => (promise, value) => {
// do something here
};
util.createPromiseHook({
resolve: makeHook('resolve'),
rejectWithNoHandler: makeHook('rejectWithNoHandler'),
handlerAddedAfterReject: makeHook('handlerAddedAfterReject'),
rejectAfterResolved: makeHook('rejectAfterResolved'),
resolveAfterResolved: makeHook('resolveAfterResolved'),
}).enable();
const x = (async () => {
throw new Error('A');
})();Each of the APIs have their merits, and each have their warts. In the discussion threads for each of the PRs, the authors have argued their cases for why theirs in the right approach and we need to come to a conclusion on which API we want to move forward with. Given the disagreement thus far and the inability to come to resolution, the decision is likely to fall to the @nodejs/tsc unless the authors of the two PRs can come together and work through some compromise approach (which is what I would like to see).
Personally, I'm good with either of the two PRs. None of the arguments in favor of either has swayed me for or against either of them.
When discussing this issue, please separate these three individual aspects so that any one of them does not get lost in the discussion.
Activity
mmarchini commentedon Sep 12, 2018
Also pinging @nodejs/diagnostics and @nodejs/post-mortem since there are interested parties there who are not core collaborators.
mcollina commentedon Sep 12, 2018
Both PRs are fine code-wise.
I am in favour of warning and crashing. I think warning adding that warning is good. I've seen plenty of code that had very hard to spot bugs because of that. It is a very tricky situation. It also encourages bad practices, like
on('data', resolve)(this is hitting theresolvefunction for every chunk, while only the first is returned).Node.js is built under the assumption that crashing in case of an uncaught exception is the right thing to do:
Not doing this for promises is a real world problem in a lot of companies adopting Node.js for writing server-side applications. Note that the following code could introduce a memory or file descriptor leak in case of errors:
This is no different than:
(this is not an express problem BTW, but I'm using express to showcase a very common pattern).
Considering how Node.js is built, crashing on
'unhandledRejection'is the safest thing to do by default to avoid leaks.misterdjules commentedon Sep 12, 2018
I've done some research internally at Netflix around this, and wrote a report about it. That survey doesn't map 100% to the questions in this issue but some of the conclusions do:
As a result of doing that survey my position is still that Node.js should provide a default of exiting on unhandled rejections (regardless of whether they are garbage collected) that is opt-out.
Please make sure to read the disclaimer before looking at this report.
I found collecting that data and doing interviews internally to be very helpful in trying to understand what is acceptable and unacceptable for users. I would really like if we could do a similar survey at the scale of the broader Node.js users community.
I know that nodejs/user-feedback#77 was started to do that, but it seems to struggle to get traction. I think if we went through that survey it could help us to get a better picture of what our users care about.
If there's a way I can help with that I'd be happy to continue contributing to that effort. /cc @nodejs/user-feedback
devsnek commentedon Sep 12, 2018
@mcollina warnings in console don't work because they can't be revoked. you also can't revoke exiting the process. the
unhandledRejectionevent makes no sense without therejectionHandledevent, and it annoys me that people ignore half of the API.@misterdjules your survey is cool, but it still only represents 32 engineers at the same company working on the same infrastructure. We will definitely need more data to start drawing conclusions.
misterdjules commentedon Sep 12, 2018
@devsnek
I think that's what the disclaimer that I linked to is saying, was that not clear when you read it?
I also mentioned that:
So I think we're on the same page 👍
mcollina commentedon Sep 12, 2018
Over the last 2 years, in almost every consulting engagement I have been part of I had to fix significant production issues related to promise handling. This include potential DoS attack vectors. IMHO it is not safe to operate a Node.js application without a) a rigorous coding environment or b) an
'unhandledRejection'event handler that crashes the process.I think Node.js should strive to be safer for newcomers.
It would be extremely easy for a developer to always
.catch():The root for this problem is more deep, and it goes back to our callback model. Let's consider this little code:
Note that in that code, every exception that is thrown by
anotherFunction()would crash the process by default.Now a developer wants to migrate that to async/await and promises:
Note that in that code, every exception that is thrown by
anotherFunction()is very likely to not clean up some system resources properly.The problem is that a significant number of developers would "add" an
asyncin front of a function without considering that it changes significantly how that function behaves. They are just doing it because they want to haveawait. I have seen this happening all the time, even by experience developers.devsnek commentedon Sep 12, 2018
@mcollina
I agree, of course. I want to make sure whatever we expose doesn't confuse newcomers any more than they already are by being inconsistent or breaking valid patterns of programming with JavaScript.
So maybe we should be looking into developer education instead of putting training wheels on features by default. If I want to use a valid model of programming in JavaScript I shouldn't have to override node to do it.
We could even release an npm module to describe as many opinionated behaviours as we wanted. Honestly that would probably make the most sense, given how much of this is based around opinions around certain programming patterns. My or @BridgeAR's PRs would enable some pretty cool stuff to happen in userland, and I hope it would also fulfill your requirements, given how often you mention that things that can be done in userland should be done in userland.
Possible API:
P.S. only sith deal in absolutes 😉
Qard commentedon Sep 12, 2018
I'm mostly in the fail-fast camp, thinking we should just crash when an unhandled exception occurs and provide a no-really-its-okay escape hatch for when you are really sure you know what you are doing. Approaching it the other way around just makes Node.js more and more a platform on which you are likely to shoot yourself in the foot. 🙀
ronkorving commentedon Sep 13, 2018
On warnings:
On crashing:
devsnek commentedon Sep 13, 2018
I couldn't agree more. +100000000
Promises are designed such that they don't halt execution when they reject. A camp of users (myself included) feels that by node shouldn't be infringing on JavaScript design goals by default.
I of course agree that we should have good debugging utilities, but I don't think they need to be killing processes and cluttering my console by default. Tooling like ndb and such can provide all the stuff we want better than core can do by itself, so I don't really understand why we wouldn't want to focus on making things easier for tooling, instead of trying to provide strange defaults.
jasnell commentedon Sep 13, 2018
ronkorving commentedon Sep 13, 2018
@jasnell Excellent point. I guess then the question becomes: is the tooling good enough? IMHO, it's good enough to avoid having to make the case for a new dedicated event. I'll end my argument there :)
mcollina commentedon Sep 14, 2018
@devsnek I'm talking about problems that our users are having today. I've seen this happening so many times in the trenches. It is not about having training wheels, it is about developer productivity and making things simpler to build. I want people to not waste days of work because they could not find the switch to make the most popular frameworks and modules to work well in production.
I think there are standards and specs, and there is software that is built with them. I value the individual developer more than standards and specs.
I think this is a bold sentence. How come Node.js does not provide a valid model of programming JavaScript? It is used by millions of developers all around the world.
The majority of our current API do not work well in a world of "on error resume", does this makes Node.js not a valid model of programming in JavaScript?
21 remaining items
misterdjules commentedon Sep 17, 2018
@benjamingr
Did we get consensus on "exit on GC"? I thought that wasn't the case.
devsnek commentedon Sep 17, 2018
until GC actually solves the "promises rejected without being detected" problem (and it probably never will), there will not be any consensus on GC.
Fishrock123 commentedon Sep 21, 2018
You can solve that problem by just logging out remaining unhandled promises on application exit.
benjamingr commentedon Sep 25, 2018
We did at some point (at the summit) but we absolutely don't have to do that - again, I'm also absolutely fine with crashing on nextTick if that's acceptable. I think we should just present all options to the TSC and bring it to a vote at this point.
misterdjules commentedon Sep 25, 2018
@benjamingr
OK, I had the impression that the public discussion at #20097 showed that there wasn't consensus on "exit on GC".
Fishrock123 commentedon Sep 28, 2018
@devsnek Please elaborate how this is 👎, it does most certainly fix that problem.
devsnek commentedon Sep 28, 2018
@Fishrock123 because GC doesn't run on exit. We would either need to make new APIs to force the GC to run and then wait for that at every exit, or keep an additional list of rejected promises that was somehow weak. both options seems like a degraded experience and I can't imagine saying "yes this is a good way to debug promises" while being potentially spammed with random console warnings when my application exits.
devsnek commentedon Sep 28, 2018
i would really like if we could try to think of other ideas besides GC. its an annoying box that leads to weird solutions that don't really make things better for users.
one quick idea: add a new section to ndb which lets you visually track promise rejection.
another idea: we already expose these events, so let userland go crazy, we can just focus on giving userland the stuff they need to go crazy.
another one:
--trace-rejectionchecks at the end of a tick if a promise has rejected without any handlers and crashes.i'm sure we can come up with nice stuff.
benjamingr commentedon Sep 28, 2018
Would you please refrain from such statements? They are needlessly decisive and focus on what you don't want to happen ("GC on exit") rather than technical arguments for/against solutions.
We are certainly not "in a box" and all ideas and suggestions are welcome.
This was tried at some point but did not turn out to be useful enough (cc @paulirish - can you or someone else do a postmortem?)
The discussion here is about the default behaviour if I understand correctly.
👍 ❤️
benjamingr commentedon Sep 28, 2018
We can do "warn on unhandled rejections on next tick and exit on GC" which is what we talked about in the summit - the problem is that some people are against exiting by default (like Jordan) and some are against waiting for GC (like Julien).
I'll be back from holiday in a week and would love to bring this up to the TSC and work on the survey - I was away for a month and wasn't in a great place personally before (family illness) the two months prior but I want to move forward on this at the second half of October.
Edit: to clarify that is not a problem with Jordan or Julien - the problem is that we don't have consensus on the end game - the discussion itself is constructive and I appreciate it.
misterdjules commentedon Sep 28, 2018
@benjamingr
Just to clarify, my concern with exit on GC is not the fact that there is a need to "wait", but the fact that it leads to nondeterministic exit codes/status.
I hope things are getting better @benjamingr, take care! ❤️
jasnell commentedon Oct 25, 2018
This was resolved
misterdjules commentedon Nov 7, 2018
@jasnell Do you mind elaborating on how this was resolved? I think it'd be helpful to mention it here for people who are not familiar with recent discussions on that topic.
My understanding is that the current default behavior will not change, and that additional opt-in behaviors (crash on next tick and silent mode) will be made available via command line options (from #20097). Is that accurate?
research: add unhandledRejection warning