-
Notifications
You must be signed in to change notification settings - Fork 29.7k
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
Terminate process on unhandled promise rejection #20392
Comments
@aduh95 thanks for your interest! there is ongoing discussion about this (from this week) in #20097 Your participation and review there are more than welcome. Please note that I've found it a rather complex subject that appears simple and I really want to make sure that we get it right.
I believe the work @BridgeAR has done in #20097 (and the prior work by @Fishrock123 ) is excellent and we're moving towards a solution to all these problems:
When testing this a while back - it proved to be a significant improvement of UX. |
IMHO it would be better to throw an unhandled exception instead of immediatelly terminating the process. The exception could then be processed in |
That's the plan, and what we decided to do together in #20097 - please consider going over the semantics there and leaving a comment - we are very open for feedback. Note it's a pretty lengthy read - sorry for that - I tried to outline most of it in #20392 (comment) but I missed some stuff like uncaughtException |
Hi,
I do some work, after all promise fullfilled and catch if one of the promises where rejected. |
Hi, it is impossible for the code above to generate an unhandled rejection since you are handling it (in a Feel free to make a gist with an mcve and I promise to look into it. In any case - terminating the process only happens on GC when Node can prove that the rejection will never be handled. |
@benjamingr Hi, you're right. I checked this situation again and realized I missinterpreted it. Thank you for claryfying, that it is impossible! It makes future searches on deprectaion warning easier becaurse of knowing the promise chain has to be someware broken. |
I want to add a quick note of caution before things proceed too fast. This breaks a really important feature of the There are many useful patterns that use deferred handlers, and they're employed across hundreds of packages. These will start crashing with this change, and I think it's important that maintainers can quickly fix these without scouring the internet. I was writing demos here (as this issue was pretty easy to find), but it got a bit long, so I moved it to medium. EDIT: This is particularly important for libraries that support both callback and (native) |
@mike-marcacci i just read your article but this is not how things are planned to work in Node.js 11. It is not even clear if anything changes in 11 but one thing is clear: the default would not crash with your example. In this case nothing will change besides not receiving a deprecation warning anymore. The reason is that it is possible to either have false negatives or false positives and the default should be safe to use. |
We've actually surveyed the ecosystem quite extensively and have found it to be used very rarely in the wild if at all. In fact, people seldom silence the unhandled rejection warnings at all - a lot of people just terminate the process on first Also, like @BridgeAR said - none of your examples crash in Node.js 11, all of them work as you intend. Node.js 11 promises will (hopefully) crash on GC. |
Hey @BridgeAR and @benjamingr – thanks for your replies. I deleted that post shortly after your comments to avoid sowing misinformation. I'm really glad to hear I had misinterpreted the deprecation message, but I'm quite sure I'm not alone in that interpretation. Is the warning implemented differently than the planned crash? You mentioned that this is safe: const promise = Promise.reject(new Error("Something happened!"));
setTimeout(async () => {
// You want to process the result here...
try {
const result = await promise;
console.log(`Hello, ${result.toUpperCase()}`)
}
// ...and handle any error here.
catch (err) {
console.error("There was an error:", err.message);
}
}, 100); ...but it currently warns that this behavior is deprecated and will crash in future versions of node. |
@mike-marcacci that's a bug in our current implementation that Ruben's work will solve. Note that by all means you should not write code like this, the fact we alllow it to error on the safe side as a platform doesn't mean it's a good idea in general - it makes tooling a lot harder and it will still warn (just without the deprecation warning). This sort of code is risky IMO |
@benjamingr - thanks for confirming this. I agree that my above code is totally unnecessary, but it illustrates the mechanics behind more complex patterns that are extremely difficult to implement without the While I totally believe you've done your due diligence in surveying the ecosystem, I personally have used these patterns in a number of different projects (both open and closed source), and there are cases where they will crash even on GC. Is my "hack" still a valid technique for bypassing this behavior when I'm absolutely sure it's the right thing to do? const promise = Promise.reject(new Error("Something happened!"));
promise.catch(() => {}) I certainly don't want to promote an anti-pattern or mislead people into thinking that this technique is generally a good idea; but at the same time, I want to make sure developers are equipped to handle this change when it's appropriate. |
its worth noting that attaching catch to promises at any time is part of the design, not by accident. i wouldn't consider a rejection truly unhandled unless the promise was gc'd without being handled. (which i think is what @BridgeAR's pr does?) |
Yes. That is entirely appropriate and the way to do a "late binding" of a
I don't think we'd have this API if we did this today - things looked very different when the aplus spec was made. Node is entirely at liberty to limit the design the same way we crash the process on errors (which browsers do not).
That is correct |
This comment has been minimized.
This comment has been minimized.
@flaviomx14 hey, this issue is not for support on Node.js problems. I recommend asking on https://github.com/nodejs/help/issues. Namely this issue tracker is for Node.js core development and not for support (there is a separate one for that which you're very welcome to use). I'm going to go ahead and hide your comment as "off topic" and welcome you to post in nodejs/help. Cheers. |
Was going to file a separate issue but this one already existed... Due to #26599 we now will have a flag going forward I am proposing that @mcollina, consider this my follow-up. :-) |
i'm still not convinced that it is the runtime's job to deal with "lazy" or "naive" coding. i mean, it doesn't warn every time you use == or call hasOwnProperty directly on an object... i don't see how this is any different. the runtime should just be focused on being correct (and in this case dying on unhandled rejections would mean some valid js code wouldn't run in node by default, which seems undesirable) |
I disagree. Some times things that are possible should AFAIC only be possible as an opt-in. This is especially important since the language is used in lots of different contexts and as server-side language it's normally best to better be safe than sorry. I believe we should have defaults that serve the majority of our users best and in this case I am very certain that most people prefer to use the I'll see if I add another short session for this topic at the collaborator summit. |
Sorry for the noise, but I do not know where the documentation for recommended patterns to avoid triggering the message falsely are. I am concerned that this pattern is still unsafe with the new behavior: async function doSomethingAsync(x) {
throw new Error(`fail ${x}!`);
}
(async () => {
const doSomething0Promise = doSomethingAsync(0);
const doSomething1Promise = doSomethingAsync(1);
try {
console.log(`result 0: ${await doSomething0Promise}`);
console.log(`result 1: ${await doSomething1Promise}`);
} catch (ex) {
console.error(`failure: ${ex}`);
process.exitCode = 1;
}
})(); I value this pattern because it allows concise concurrency. I recognize that, in many cases, the rejections will not be “handled”. And the IIFE will exit without waiting for all promises to complete (making it different from |
@binki This allows concurrency, but waits for all promises to resolve/reject: const all = ps => new Promise((res, rej) => {
const arr = ps.map(() => null);
let n = 0, ok = 1, err;
ps.forEach((pr, i) => {
pr.then(r => arr[i] = r)
.catch(e => ok && (ok = 0, err = e))
.finally(() => {
if(++n === arr.length)
ok ? res(arr) : rej(err);
});
});
});
const doSomethingAsync = async x => {
throw new Error(`fail ${x}!`);
};
(async () => {
let results = all([
doSomethingAsync(0),
doSomethingAsync(1),
]);
try{
results = await results;
console.log(`result 0: ${results[0]}`);
console.log(`result 1: ${results[1]}`);
}catch(ex){
console.error(`failure: ${ex}`);
process.exitCode = 1;
}
})().catch(console.log); |
@blinki that code has a real big - if the second function rejects you don't handle the error which has big concequences Consider Promise.all |
@Hakerh400 @benjamingr In both of your solutions, you change the behavior of my code. You force the first I can see how my function not waiting for all of the |
Since this conversation has been hitting my inbox again, I want to reiterate how bad of an idea I think this change is. There are many scenarios in which a library might want to return a promise that the user may choose to handle or not.
Fundamentally, I think promise rejection is substantially different than "throwing" under normal synchronous flow. Let's take this for example: function a() { console.log("a"); return 1; }
function b(input) { throw new Error("oops"); return input + 2; }
function c(input) { console.log("c"); return input + 3; }
function run() {
const resultA = a();
const resultB = b(resultA);
return c(resultB);
}
console.log("before");
const result = run();
console.log("after", result); It's abundantly clear how the This scenario is very similar: async function a() { console.log("a"); return 1; }
async function b(input) { throw new Error("oops"); return input + 2; }
async function c(input) { console.log("c"); return input + 3; }
async function run() {
const resultA = await a();
const resultB = await b(resultA);
return c(resultB);
}
console.log("before");
const result = run();
console.log("before 2"); ...but has a critical difference: the expression To be perfectly frank, this proposal seems far more about creating the appearance of safety than addressing an actual deficit in application correctness. I'm not questioning the value in detecting unhandled promises (resolved OR rejected) as a development tool for calling attention to a potentially undesired flow... but just like other lint rules, this belongs in tooling and NOT the execution environment. Just my 2 cents. |
A better pattern is to not return a promise if a callback is passed in. If the internals themselves use a promise, do |
This is a semver-major change that resolves DEP0018. This PR defines a new mode for the --unhandled-rejections flag, called "default". The "default" mode first emits unhandledRejection. If this hook is not set, the "default" mode will raise the unhandled rejection as an uncaught exception. This mirrors the behavior of the current (unnamed) default mode for --unhandled-rejections, which behaves nearly like "warn" mode. The current default behavior is to emit unhandledRejection; if this hook is not set, the unnamed default mode logs a warning and a deprecation notice. (In comparison, "warn" mode always logs a warning, regardless of whether the hook is set.) All users that have set an unhandledRejection hook or set a non-default value for the --unhandled-rejections flag will see no change in behavior after this change. Fixes: nodejs#20392 Refs: https://nodejs.org/dist/latest/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections
I think I've figured out a pretty good compromise solution in PR #33021. I invite even the most cantankerous among you to review it. It defines a new |
I have an idea: trigger a BSOD on unhandled promise rejection... just to teach users |
TSC will be voting on the intended default behavior for unhandled rejections on v15, where we also intend to remove the deprecation warning. We want to listen to Node.js users experiences with unhandled promises and what you think should be the default before voting, so we prepared a survey: https://www.surveymonkey.com/r/FTJM7YD We also wrote an accompanying blog post for extra context: https://medium.com/@nodejs/node-js-promise-reject-use-case-survey-98e3328340c9 The survey will run for at least two weeks, at which point we'll evaluate if the number of replies is enough for us to move forward, otherwise we might extend it for a week or two. Please fill out the survey as it will help us decide the future of unhandled promise rejections on Node.js! |
@mmarchini I'm not sure if you have the power to fix this, but the Medium post has weird line breaks (presumably from being copied and pasted from plain text). |
This is a semver-major change that resolves DEP0018. All users that have set an unhandledRejection hook or set a non-default value for the --unhandled-rejections flag will see no change in behavior after this change. Fixes: nodejs#20392 Refs: https://nodejs.org/dist/latest/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections
This is a semver-major change that resolves DEP0018. All users that have set an unhandledRejection hook or set a non-default value for the --unhandled-rejections flag will see no change in behavior after this change. Refs: https://nodejs.org/dist/latest/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections PR-URL: nodejs#33021 Fixes: nodejs#20392 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Mary Marchini <oss@mmarchini.me> Reviewed-By: Shelley Vohr <codebytere@gmail.com> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Anatoli Papirovski <apapirovski@mac.com> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
The survey seems to be closed, so I'll post my feedback here, hopefully you'll find it helpful: Today I ran a project after updating nodejs to const whenSomeOptionalAsyncData = getSomeOptionalAsyncData();
...
return {
getSomeOptionalAsyncData: () => whenSomeOptionalAsyncData,
} terminates my application (moreover without a clear stack trace, as I fail to understand what is wrong with such coding pattern to require terminating the application: the API allows you to either retrieve the cached data if it was fetched successfully or get a rejection with explanation of why it wasn't. To preserve such behaviour while complying to the new unhandled promise shutdown, I would have to come up with some wrapper structure to persist the error from the rejection or smth. Maybe nodejs team could consider the direction taken by eslint/no-floating-promises of not treating promises assigned to a variable as unhandled? If developer does not operate on the promise stored in the variable anyhow, the linter/IDE will warn him that it's unused, while when he will, he'll get the shutdown check again when he'll try to use this variable, so no unhandled promise coding mistakes should be missed still. Of course I can always manually add an |
@klesun Node simply registers a callback to V8’s API for HostPromiseRejectionTracker, which causes the program to terminate if V8 determines that no rejection handler is being added to the promise. To fix your case, you need to attach a no-op handler: const whenSomeOptionalAsyncData = getSomeOptionalAsyncData();
// Attach a no-op rejection handler, so that the process doesn't exit
// if `whenSomeOptionalAsyncData` is a rejected promise:
whenSomeOptionalAsyncData.catch(() => {});
...
return {
// Use async function to create a new, possibly rejected promise without
// the above rejection handler, so that code which uses this API
// is required to attach its own rejection handler:
getSomeOptionalAsyncData: async () => whenSomeOptionalAsyncData,
}; |
I feel like calling these "unhandled" promise rejections are a bit of a misnomer (ie. bad naming). My understanding is that these are actually closer to "not yet handled" promise rejections as it is still entirely possible that they will be handled by a subsequent await/catch and then the program could continue to run as normal as demonstrated in: Similar to successful promise resolution, it would be normal to expect that the rejection handling would also follow the same path and keep the rejected value until the associated promise is awaited on at which point its value can be returned to the enclosing try/catch block and handled accordingly. Instead, it appears that Node is intercepting these rejections too early (during the earlier awaits) and causing the process to exit with a failure code (please see issue linked above). I understand that this can be worked around by defining my own unhandled promise rejection handler or by setting certain node flags but this should not be the default behavior as Node is causing programs to stop under completely normal Promise resolution conditions. I also understand that I can avoid this behavior by grouping my awaits with a Promise.all but this may not always be possible depending on the order of operations (awaits) required to solve a given problem. For a promise to be considered truly "unhandled" then I believe Node should wait until the associated promise has no more references and/or is garbage collected before determining that its rejection has really and truly been unhandled and only then terminating the process. (I believe this may have been suggested by earlier posts). Please correct me if I'm misunderstanding the situation or please fix. |
When a Promise is rejected and has no catch handler, the following warning is displayed on stderr:
This behavior is in place for quite some time now I believe, and maybe it would be time to actually make node exit on a non-zero code.
The text was updated successfully, but these errors were encountered: