-
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
No correct stack trace when reject value isn't an instanceof Error #41676
Comments
I discovered while upgrading from node@10.9.0 to node@16.13.2 and running jest tests jest@24.9.0 |
Stacks are generated when an error object is created. When a non-error is thrown (or rejected) no stack trace is generated. This isn't specific to Node it's inherent to the design of errors in JavaScript. I think you've just run into why "Always throw (As a side note - you will get a stack trace if the debugger is connected when inspecting the error in the debugger but that's not a valid workaround in general - for example for seeing errors in your jest tests in CI) |
Also thanks for the detailed feature request and thorough explanation of where you ran into this! |
We should fix cross-realm errors (but not primitives) and change the Edit: PR to fix the jest issue #41682 |
@benjamingr Thanks for the quick reply & PR! really impressive. However, while I agree on most of what you said, I must disagree on the primitives thing. The only difference is as you said, errors contain stack traces on the object itself so the I think of if from a usability & dev x perspective. I can think of some implementation maybe that the I totally agree that the best practice is rejecting with errors. Therefore, I think all cases and all reject values should be handled and have enough information to be discoverable. |
Browsers only handle that case with the debugger connected - if you throw an Error without the devtools open and watch it in your instrumentation you will see it has no stack trace. This is because this is both rare and incurs a performance overhead and because the debugger can "cheat" and do things that aren't standardized in JavaScript itself. Node does this as well which is why I suggested running with the inspector and pausing in the debugger as a workaround locally. We can probably log a better message with an inspector connected (calling the relevant cdp function)
This is equally true (and has been for 10+ years) for regular uncaught exceptions and it's a property of JavaScript.
That can't work since stacks in JavaScript are a property of the Error object itself rather than the
I opened a PR for that :) It seems very reasonable to me to support cross-realm errors. It will sit around for another day and then I'll push test fixes, solicit reviews and merge it.
I agree and if it was possible to entirely prohibit rejecting non-errors I would turn that switch. In bluebird (which I maintain(ed) before) we used to warn on this (with a stack trace in non-production builds). I also don't agree with the argument regarding the ecosystem - every npm module can call |
So to summarize my position:
|
Not sure what you meant by "your instrumentation" exactly but i tried doing this in the browser using The Node api is a bit different and doesn't have this event and instead uses only the reason and the promise.
However i tried this on a regular throw statement like window.addEventListener('error', function(e) {
console.log(`Error: ${e.error}; thrown at: ${e.filename}:${e.lineno}:${e.colno}`); // => Error: This is not an error; thrown at: http://127.0.0.1:4200/index.js:5:1
})
throw 'This is not an error' The error property on the event itself is indeed not an error instance (it's just a string of course) and doesn't include a stack trace for that reason. But you could still construct a valuable reference to where the error occurred and perhaps send it to your monitoring service (Datadog etc..).
That sounds cool but it doesn't solve the most important case - production.
Found this little lib that does something similar to what i said: https://github.com/grantila/trace-unhandled/blob/master/lib/core.ts#L128
Yep i saw! thanks for that again - for so quickly jumping onto this
True, but doing those things (
I'll be glad to. I might reach out to you sometime soon. |
when node intercepts an uncaught exception that isn't an Error it can take a snapshot of the current stack to provide a traceback but that's not possible for the promise case because promise executors start from an empty stack. and even so it only does that when the uncaught exception is fatal. if you handle it with an event you don't get that, as you noticed. |
This was the proposal to unify unhandled rejections across promise libraries for better DX: https://gist.github.com/benjamingr/0237932cee84712951a2 (the formatting is dead, sort of because of markdown changes in GitHub). I implemented it in promise libraries (like Q which was popular) and asked @petkaantonov for help with it in Node core (I wasn't a collaborator back then) - which resulted in #758 - there were several talks with different parties (namely nodejs/promises nodejs/promises-debugging and nodejs/promise-use-cases initiatives + collaborator summit sessions) which helped motivate things like async stack traces. Additionally the last push (an initiative by Mary) to finally settle "what should Node do" and agree to crash the process finally making async exceptions behave like sync exceptions. Node didn't have EventTarget back then (it was added later) and still doesn't have Window or anything comparable so it uses Node event syntax (like uncaughtException) rather than the DOM's. The person who specified the DOM's event helped us with ours (and the feedback/research) helped theirs).
I'm... not sure how you're doing that but I get
I connect debuggers to production machines all the time, it's actually pretty easy (you can send a node instance a signal to open the debugger then expose the port from docker and connect to it - the inspector is just a websocket). That said, the solution is simply not to throw strings - that's as bad as doing
We did that at bluebird (https://github.com/petkaantonov/bluebird) the trick is you capture a stack whenever a promise is created, keep track of chains and if that promise is rejected you "stitch" the chains and has its own context stack (here: https://github.com/petkaantonov/bluebird/blob/e8006fc50dcfd6323639e2864aac9b52a9ec5466/src/context.js#L45 ) - it even used to work for primitives! The thing is - incurring overhead (a stack trace is expensive) pre-emptively for an extremely rare use case is probably not worth it for the same reason async stack traces don't work for anything other than As a fun exercise you can build this in userland using async_hooks where you get events for promises (there is a performance penalty though keep in mind!) see the promise tracking section for details.
We also used to wrap non-errors when
Please do! |
Support cross realm errors where `instanceof` errors in our unhandledRejection warning to print better error with stack traces. PR-URL: nodejs#41682 Refs: nodejs#41676 Reviewed-By: Nitzan Uziely <linkgoron@gmail.com> Reviewed-By: Tierney Cyren <hello@bnb.im>
Going to go ahead and close this - when you want to work on better support for primitives please let me know and I'll reopen :) |
Version
15.x.x, 16.x.x, 17.x.x
Platform
Darwin ItamarGro-2.local 20.6.0 Darwin Kernel Version 20.6.0: Mon Aug 30 06:12:21 PDT 2021; root:xnu-7195.141.6~3/RELEASE_X86_64 x86_64
Subsystem
No response
What steps will reproduce the bug?
Node doesn't seem to add relevant stack trace information to where the error occurred in the case of unhandled rejection with a value that is not instance of
Error
.This is a case that reproduces this undesired behavior:
run this with
node index.js
:The output of this script is:
This is problematic because in the output above there's simply no information to help me understand where the error occurred.
In such a small script it is obvious where the problem is but in a larger codebase it's basically impossible to find.
I found it only by debugging and running through the functions.
In contrast, rejecting with a value that is an instance of
Error
will throw a stack trace correctly:This script will output:
In this case i know where the problem is and i can fix it.
How often does it reproduce? Is there a required condition?
It reproduces in node 15, 16 and 17 (with default value for flag
--unhandled-rejections=throw
and not--unhandled-rejections=warn-with-error-code
or some other option) on every unhandled rejection with a value that is not instance of error.What is the expected behavior?
It would be nice to at least get a stack trace to the place in my code where the reject happened.
something like this:
What do you see instead?
I see a non descriptive error message that doesn't explain clearly where the problem is:
Additional information
Basically the mechanism for throwing on uncaught rejections is nice but i think it's missing handling those edge cases where reject value isn't a native error.
The text was updated successfully, but these errors were encountered: