-
-
Notifications
You must be signed in to change notification settings - Fork 269
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
[FEATURE] Can't see cause or aggregate details for uncaught errors #1000
Comments
I'm noting that this is probably related to tapjs/stack-utils#61, which has a whole ton of discussion about cause formatting in general. It's probably the place to go to discuss specifics on that mark! (See also jestjs/jest#11935.) But I'm keeping this issue around as tracking for other ways to integrate custom or Node.js-built-in error tracing for tap itself. |
I've done some digging in the source to try to get more familiar with basically how tap works and where changes would be needed, and the give-away (after digging in the reporter code) is that the reporter is most certainly parsing the tap results, with no extra "magic" after the fact to generate new information, so the core test runner is responsible (which generates tap output) is responsible for stack tracing. Specifically, the function we've gotta look at is I'm not sure what the ideal solution would be for embedding information that isn't a plain stack trace into the results here. I guess the good thing is that YAML Diagnostic blocks aren't standardized? So we can choose to add completely new information here, without altering the existing expected contents of Personally, seeing as it's a structured YAML object anyway, I think we should basically allow recursive details here, exactly the same as the current flat shape is reported, just without Of course, all this means that as-is, tap's error reporting for result details is handled by Actually, if
I'm definitely inclined more towards the second approach, because it keeps I might give implementing all of this a shot, since implementation-wise it doesn't sound dreadfully complicated (recursion in TypeScript isn't that hard, right!?), and would be a fun project to get familiar with tap's internals — even if any of this is out of scope for tap or some totally different approach is selected. But no guarantees I'll get anywhere with my code! 👾 |
We did it! We still need to do test cases, and probably some extra-detail adjustments (it doesn't display the error type, nor any Also some checking against Node.js to make sure everything is, in fact, working precisely as intended (that's where we're pulling the essential appearance, "42 lines matching cause trace" messaging, and a chunk of implementation code from). |
I'm starting to use Error.cause pretty extensively now, and moving everything away from I'd like to get this fully integrated throughout the stack of things that handle Error objects, and use
With the top and bottom of the chain visually brighter/bolder, since that's what you want your attention drawn to. Sometimes (when it's not an Error) the |
Thanks for your attention on this and keeping up with error causes in general! They're an awesome feature I love to see getting adopted more. Just a nit about your example, to make sure it's getting communicated properly:
I'm noting that the non-Error object you're displaying here itself has a
If we don't do this, then a non-Error If we do support it, I think that makes for better expressiveness, but I'm noting it's sort of a new/benign interpretation of the spec (InstallErrorCause etc), which doesn't mention that an error's cause has to have literally any particular value/shape. (So, doesn't provide meaning to that value, etc, though clearly browsers choose to interpret it by showing a chain of errors, for example.) IMO cause chaining is a fairly popular use of error causes, so since causes support any value (not just errors), we might want to say "any
Generally I'm a huge fan of this! Just, you might want to bring some aggregate errors into your examples, too (you've only got a non-aggregate cause chain above). Aggregate errors always represent multiple errors (and nothing more than multiple errors), so the only semantic way to show them is to display those errors. E.g, if the cause of an error is an aggregate error, then the cause is multiple errors, so we have to display the multiple errors. But I agree that displaying full stack traces can get extremely noisy, and that's exactly why we wrote a custom A couple of thoughts related to verbosity here:
My suggestion would be to use an environment variable to increase the default filtered verbosity, which also affects the generated tap results file (because re-running a test isn't that inconvenient), but maybe you have a better idea!
Yeah, good point. Does node-tap handle the string/yaml-ification of arbitrary objects anywhere else (outside of diffs) yet?
What would a less verbose arbitrary object look like, though? If we'd like to represent a "useful" subset of the properties, that depends on knowledge based on the constructor, which is totally out of scope for node-tap's internals and project work IMO. But probably a problem that's already being tackled by other people, e.g. whoever makes objects in chrome's devtools useful? Maybe there's a library for this? If we don't make any guesses about which of the object's properties are useful, we could display:
Looking forward to see where your experience with causes goes in general! I think tooling which not just supports but is itself built around causes is still pretty rare, so we're super interested to see how it develops. |
Haha, yes, my dummy text should not have included a |
It's been around a while now, but it's still a somewhat new feature, as far as the language goes. It just arrived in Node 16.9, so it's only pretty recently that all non-Error.cause-supporting node versions have fallen out of the common support set. It'll probably for some time still be more common to see With any large program that does a lot of fs and network stuff, handles user input that must be validated, calculates things that might not be resolveable, etc, you end up needing to have some code in a single place that reports errors usefully to the user, but also there's a need to keep the separation of concerns. This tension in npm was resolved with a hodgepodge of duck-typing and some convention around known With vlt, we're trying to make this a bit more orderly. We're using an |
That's awesome. I love the comparison between how things have historically been handled in Node.js (A for effort, but...) and the new capabilities that such a simple idea, "a cause that can be anything" (and alongside that, cause chaining) can help bring. We've been seeing really similar semantic benefits in hsmusic too. We've exclusively set error causes to be It's been a couple years so I honestly don't remember how we did validation before, but it was certainly a lot less flexible and informative than today. Since hsmusic is fundamentally a "convert a static data repository into a static wiki website (showing loads of relationships, presentational details, general info, etc)" project, data validation is one of its most important duties. Error causes and aggregate errors have gone a long, long way to make the results of that validation explorable, traceable, and useful! |
import t from 'tap'
const e = new Error('hello', {
cause: new Error('xyz', {
cause: {
some: 'stuff',
cause: new Error('deeper', {
cause: true,
}),
},
}),
})
t.test('parent', t => {
t.test('child', t => {
t.error(e)
t.end()
})
t.end()
}) Here's what I got so far, still pretty rough and a bit noisier than I'd like: http://167.99.100.141/tap-cause-output.html Indentation makes it kind of crazy when you have a lot of things, and I feel like just adding the separator makes it clear that it's a chain of events. I'm not doing anything now to elide out the intermediate causes, but I think you're right, that they should ideally be preserved as verbose as you'd likely ever want in the raw |
This is very stylish! I see that the bold line has the text "Cause:" and then the message, for error-based causes. I love the separator and think it's a great fit if cause entries will frequently be more than one line long—indentation works for us because of how hierarchically structured it is (lots of aggregate errors) and how few extra details we include (not even the top line of traceback in most instances). Just using a separator line is sleek and simple at conveying a chain. When aggregates are output, I think indentation is necessary as a baseline, since that's a tree structure - otherwise you'd be looking at something like XML without indentation, which is Do you think the This isn't to say that other properties should be filtered out, necessarily (e.g. a subclass of Error that provides other useful keys/values), but if you're specially showing "cause" and skipping that in the property list, you can probably do the same for "message" and get a more concise output. Also, the bottom entry - visually the last in the chain - has a cause. It's a cause which isn't an Error, sure, but we've already seen that those are displayed within the chain just like Errors are (
I get why the final I'm also noting some confusion over the ordering of lines. As far as I can tell, based on these examples:
The order appears to be:
To me the practical use for showing both Like I mentioned above, I think you can drop the
Order being:
With the code context, too, for a more direct comparison (I am a
|
First pass at this, still a bit noisier than I'd like, and similar support is needed for AggregateError. Re: #1000
The reason why it's showing It's exceedingly rare, outside of contrived tests, to create an Error.cause right inline. Usually, it comes from some other part of the program, and having the full stack can be essential. It's already stripping out node internal frames and any frames from within tap itself (unless you're in the tap project, which I am in this case), so I'm not too worried about it. I cleaned it up a bit in main having put it through some tests, but I'm going to roll with this overly verbose option as a default for now, and then probably make the verbosity configurable once it starts to annoy me 😅 |
That makes sense! Sounds good. I still think dropping |
This also cleans up the 'Error.cause' reporting a bit. AggregateError.errors are displayed indented, to make it clear that it's an array of Error objects. Re: #1000
Landed and published in v19 |
Thank you @isaacs! 🙏 🎉 |
Is there an existing issue for this?
Have you read the
CONTRIBUTING
guide on posting issues, andCODE_OF_CONDUCT
?Description
To aid in debugging without necessitating futzing around with Chrome-style
debugger
statements, several infrastructural parts of my code wrap uncaught errors with supplemental details by creating errors withcause
.In practice we use a custom (and not-so-appropriately named)
showAggregate
function to display causes and aggregate errors in a nice, compact tree, for example below:Node.js already tries to accommodate errors with
cause
and aggregate errors, too, as below:Tap doesn't, though! All I get is details about the top-level error.
This is problematic for a few reasons, but the biggest is that — provided a simple cause chain, with no aggregate errors — I don't get any information about the bottom error. That's where the code actually failed (since it's an uncaught error). Of course, I hardly get any of the surrounding context I've added through errors with
cause
; I get the very top layer (since it's the top-level error object that tap considers), but that's it.Of course, if any layers are aggregate errors, the overall structure really is important — it's not just supplemental context anymore, but describing possibly multiple errors that went wrong. For proper debugging, I need access to not just one of them (or the top layer) but the whole structure.
In practice I've worked around this by temporarily editing the test file, wrapping the line of code that eventually throws the error in debugging boilerplate:
It works, but it's super inconvenient.
I can think of three ways to make this better on tap's part:
cause
and aggregate errors. This is probably an involved process — I don't know how much tap is built on Node's own error reporting, but if its implementation is highly customized/hand-made, it could require a lot of work to approximate or take inspiration from Node's error handling.There's also a big question of what to do with the code context that tap provides, which is genuinely super helpful, but completely fails for errors-with-cause or aggregate errors — providing only code context for the top-level (and generally supplemental) error. But obviously displaying code context for every error in a chain would get very unwieldy. (You could argue that this is why tap only shows the code context for the top line in an error trace!)
On my part there are probably nicer ways to get around this, for example by handling uncaught errors in general (
unhandledRejection
) or by putting specific error-handling code into a helper function that wraps my tests. (I already have one for snapshot tests, but most of my unit tests just use the top-levelt.test()
function directly, so those would take some not-so-nice refactoring).But I'm pretty sure workarounds I'd code on my end wouldn't be able to integrate nicely into tap's own error reporting; I basically only have the option to spit text into stdout/stderr and manually correlate it to the actual test that failed, in tap's summary, once all my tests have evaluated.
Although this gets in the way, it's not like tap is doing anything wrong here — it just doesn't specially report uncaught errors with
cause
and aggregate errors. So I've filed making any sorts of related improvements as a feature request! And I'd love to find out that actually there are ways to handle this better right now, too, but figured I'd summarize the situation anyway.Example
Simple reproduction code with only a plain
cause
chain, no aggregate errors:Note that the code context for the bottom error is around
return new TypeError("Division by zero, ma'am?");
, but the context tap provides is aroundthrow new Error("Oh dear in relation(fakeRelation)", {
. tap also doesn't give me any way to figure out the trace or even message for that bottom-level error, nor any of the errors in-between.Edit: Yay, issue #1000! 🥳
The text was updated successfully, but these errors were encountered: