-
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
Node incorrectly throws ERR_UNHANDLED_REJECTION
on error caught in Promise.catch
#43326
Comments
Nevermind, it happens with chained // test2.js
export function testPromiseCatchFinally() {
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
const e = new Error('rejected2')
reject(e)
}, 2000)
})
p2.then(n => {
throw new Error('then() should not run.')
}).catch(e => {
if (e.message != 'rejected2') throw new Error('It should have rejected with the correct error')
console.log('ERROR CAUGHT')
})
p2.finally(() => {
console.log('FINALLY')
})
}
testPromiseCatchFinally() Output:
Note that commenting out the export function testPromiseCatchFinally() {
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
const e = new Error('rejected2')
reject(e)
}, 2000)
})
p2.then(n => {
throw new Error('then() should not run.')
}).catch(e => {
if (e.message != 'rejected2') throw new Error('It should have rejected with the correct error')
console.log('ERROR CAUGHT')
})
// p2.finally(() => {
// console.log('FINALLY')
// })
}
testPromiseCatchFinally() Output:
Chaining the export function testPromiseCatchFinally() {
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
const e = new Error('rejected2')
reject(e)
}, 2000)
})
p2.then(n => {
throw new Error('then() should not run.')
})
.catch(e => {
if (e.message != 'rejected2') throw new Error('It should have rejected with the correct error')
console.log('ERROR CAUGHT')
})
.finally(() => {
console.log('FINALLY')
})
}
testPromiseCatchFinally() Output:
|
Can you check with a newer node version? Also, can you confirm/deny whether your test case is equivalent to this: const p = new Promise((_, reject) => {
setImmediate(() => reject(new Error("err")))
})
p.catch(console.error)
|
I'm trying to understand your expectation of why node shouldn't log an error here since there is a promise chain in your example without a
You have a dangling unhandled error - can you elaborate on what you think would be better behavior? |
Hmmm. The thing is that it is easy to write code this way that actually handles an error and that is totally valid within a private scope. If I am making my own private code, and the |
This is standard behavior and happens also in browsers using the unhandledrejection event. Essentially, this is working exactly as it was intended. The way to tell Node.js that such code is ok and expected it to attach a non-op catch handler or to handle the unhandledRejection event. |
In your particular case the code is not ok since This is akin to adding an empty |
I'm seeing a similar issue here, on node v18.7.0. function log(label) {
return (...args) => console.log(`[${label}]`, ...args);
}
function wait() {
return new Promise((resolve) => setImmediate(() => resolve()));
}
function indirect() {
log("inside indirect, before creating rejected promise")();
const x = Promise.reject(new Error("An Error"));
log("inside indirect, before wait")();
return wait().then(() => {
log("inside indirect, after wait")();
return x;
});
}
indirect().then(log("finished")).catch(log("outer catch")); gives the following output and stops execution halfway through:
changing the type of the rejection reason to a string gives an
|
Right and as in the original post this is intended behavior. Rejected promises need to be handled before I/O (like setImmedaiate) is processed or otherwise they're considered unhandled. You can work around this by adding an empty catch handler to the promise (forking it, not handling the error) |
Right, so just to make sure I'm clear there, the fix would look like this: function wait() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
function test() {
const x = Promise.reject(new Error("this should be caught"));
x.catch(() => {});
return wait().then(() => x);
}
test().catch((e) => {
console.log("caught error", e);
}); That's super counter-intuitive and surprising for me, and I've been writing javascript a long time. Is there some related documentation as to why this is? Some note about this quirk in the documentation anywhere? And why the traceback doesn't say "unhandled rejection" unless you use a |
@devoidfury the reason is that it's not possible to know if and when the handler is actually attached. Attaching the handler could in fact fail as well. Imagine |
@BridgeAR okay, but this code has the same problem even though it handles an imaginary function wait() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
function test() {
const x = Promise.reject(new Error("this should be caught"));
return wait().then(() => x).catch(() => x);
}
test().catch((e) => {
console.log("caught error", e);
}); [devoidfury@oryx errors]$ node demo.js
file:///home/devoidfury/projects/quest/errors/demo.js:6
const x = Promise.reject(new Error("this should be caught"));
^
Error: this should be caught
at test (file:///home/devoidfury/projects/quest/errors/demo.js:6:28)
at file:///home/devoidfury/projects/quest/errors/demo.js:12:1
at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:541:24)
at async loadESM (node:internal/process/esm_loader:91:5)
at async handleMainPromise (node:internal/modules/run_main:65:12)
Node.js v18.7.0 Okay, I get it, it's a limitation in the engine's PromiseRejectionTracker (or whatever you call it) that intentionally crashes the process when it detects an unhandled rejection (right before doing IO, apparently). Fine. All that said, at the very least, the error it spits out should indicate that the reason has to do with an unhandled rejection, in addition to just regurgitating the stack from whatever errror was rejected, no? |
Yes, it is indeed a limitation in detecting such cases. About telling the reason: it's an error, just as any other error in the application. We do not log extra information in case the process ends due to a sync error either. In case it's anything besides an error that is thrown, we attach further information for the user to know what happened as there is no way to directly know where the error came from. |
Maybe we could generate a new Error and set the reject |
I'm seeing even worse behavior with the async/await syntax. What is even going on here?
|
Correct, in bluebird we had an explicit ".suppressUnhandledRejection" - as Ruben said Node has no way to know when a rejection will not be handled. If we could - we'd solve the halting problem.
It is, we spent like 5 years debating this behavior and several competing proposals (like consider it rejected on GC) and this ended up being (by far) the least-surprising design. It is fundamentally a problem with the design of error handling in promises themselves and the alternatives (e.g. swallow errors, only err on GC etc) seemed worse. It is often better not to "kick off" background work like that and instead have a dedicated "queueBackgroundWork" function that takes your functions you want to happen in the background and centralizes error handling.
In general we already do show where the error is (errors in V8 JavaScript capture their stack trace when they are created not thrown/rejected so the place the promise was rejected should typically be there. Can you provide an example of what we can do better?
What additional info would you expect in this case? Note the code can be written to not leave dangling errors (e.g. do |
The problem I have with this is, unlike most other scenarios where an error pops up, is there's nothing in that stack trace you can search on the internet to get an answer. I guarantee there are thousands of bugs out there in userland code caused by this, where the symptom is "it crashes here once in a while in production and I have no idea why but I've already spent a week trying to figure it out so we'll just have to live with it shrugs". At least if it hinted that there was an "unhandled rejection", now we have some keywords we can search that might lead to an explanation of what's wrong with our code and how to fix it. For example, any of these would be way more debug-able in my opinion:
|
So basically, ideally you'd want us to add some text if the process exits due to an unhandled rejection like we used to have to log before we exit? @BridgeAR wdyt? |
Yes, that's exactly right, just adding some important keywords to the stack trace to make it more informative, discoverable, searchable, learnable; having the location the problem occurred is great, but knowing the name of the problem is just as important. |
Not sure if my beef is with TC39 or Node, but I hit this by having a That is: async function tryButFail() {
throw new Error('boom!')
}
const p = tryButFail();
p.catch(e => console.log('handled'));
p.finally(() => console.log('cleanup')); Even though:
...I got an unhandled rejection. I would have thought handling of the failure in As mentioned above by others, the other thing that made it hard to debug, is that the |
That's the issue, const p = Promise.reject(new Error());
p.catch(...); // p will no longer have unhandled rejections
const p2 = p.finally(...);
// without p2.catch, p2 is a continuation of p (before the .catch) and thus will be rejected
// its rejection will be unhandled
I agree we should probably improve this. To be explicit you currently get:
But would want to get:
Where (I am not sure we can do that technically, at least without async hooks or a big performance penalty for the same reason V8 only creates stack traces for async functions - but I want to understand better + maybe with the inspector attached?) |
Re: the stack trace. Yeah putting the finally() into the stack trace would be supreme. Where finally() represents the "leaf" promise. Only when inspector attached would be a very nice compromise there!
My beef might not be with Node here as I dunno what is specced. But I feel like it doesn't have to be this way? The distinction could then be made based on where the error actually occurred whether the rejection is handled. Perhaps there are similar performance reasons why you can't look back at the chain when an error occurs, though? Too many promises held? And while I have no illusion my opinion here matters - I'm sure you all have thought long and hard about this stuff - it seems to me that there's a philosophical question. Do people care that each "leaf" promise has been caught? Or that each "original error" has been handled? The latter seems more useful to me - why is it important that I log the same error twice? But I'm sure there are legitimate reasons for both. Maybe one solution is that this "original error caught" distinction is passed to the unhandledrejection handler. Would have helped my sanity in dev, and I think I would not process.exit(1) in production if I knew a rejection was caught somewhere and handled (even if it wasn't handled everywhere) |
I encountered an unhandled promise rejection error when building my React-Native iOS project using Node.js. The error message points to a failure with xcodebuild, exiting with error code 65. Below is the error log:
|
Version
17.3.0
Platform
Linux
Subsystem
internal/process/promises
What steps will reproduce the bug?
Make a project folder.
Create
package.json
:Paste this code into a file
test.js
:Run the file, then you'll see this output:
Note the output contains
ERROR WAS CAUGHT!
which shows that the error was handled by a.catch
callback, yet Node.js still exits non-zero withERR_UNHANDLED_REJECTION
.Note that if you comment out the line
const e = {message: "rejected", name: "Error"}
and uncommentconst e = new Error("rejected")
, the error becomes more obscure:Finally, note that if you comment
p2.catch((e) => {
and uncomment.catch((e) => {
so that the.catch
callback is chained on the.then
promise, the problem does not happen and the output is:with exit code
0
.How often does it reproduce? Is there a required condition?
Every time when
.catch
is not chained on.then
.What is the expected behavior?
Should Node exit with zero, because the error was caught?
What do you see instead?
1
Additional information
No response
The text was updated successfully, but these errors were encountered: