-
Notifications
You must be signed in to change notification settings - Fork 30k
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
repl: remove internal frames from runtime errors #15351
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an observation but the filtering approach in this PR is somewhat ad hoc.
If we are going to do more of this in the future, then perhaps we should revive parts of #2741: record script ids of builtin scripts and check stack frames against them.
v8::Exception::CreateMessage()
(which that PR doesn't use) lets you do that for arbitrary exception objects.
lib/repl.js
Outdated
if (err.stack) { | ||
const regex = new RegExp(/\s+repl:/); | ||
const stack = err.stack.split('\n'); | ||
const idx = stack.findIndex((frame) => regex.test(frame)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This filters only a single frame. Wouldn't it make more sense to do e.g.
err.stack = err.stack.split('\n').filter((s) => !/ repl:/.test(s)).join('\n');
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This finds the index of repl:
and on line 383 below, the stack is spliced from that point, so all frames after it are removed. If I am reading it correctly, I believe your suggestion would only eliminate lines that begin with repl:
, but that's not all that we are looking for. Here is some before/after:
$ node --version
v8.4.0
$ node
> .load test/fixtures/repl-pretty-stack.js
> 'use strict';
'use strict'
> function a() {
... b();
... }
undefined
> function b() {
... c();
... }
undefined
> function c() {
... throw new Error('Whoops!');
... }
undefined
> a();
Error: Whoops!
at c (repl:2:9)
at b (repl:2:3)
at a (repl:2:3)
at repl:1:1
at ContextifyScript.Script.runInThisContext (vm.js:44:33)
at REPLServer.defaultEval (repl.js:239:29)
at bound (domain.js:301:14)
at REPLServer.runBound [as eval] (domain.js:314:12)
at REPLServer.onLine (repl.js:440:10)
at emitOne (events.js:120:20)
>
$ ./node --version
v9.0.0-pre
$ ./node
> .load test/fixtures/repl-pretty-stack.js
'use strict';
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error('Whoops!');
}
a();
Error: Whoops!
at c (repl:9:13)
at b (repl:6:5)
at a (repl:3:3)
>
As you can see, it takes care of stack frames including REPLServer
, ContextifyScript
, etc.
lib/repl.js
Outdated
@@ -374,6 +374,17 @@ function REPLServer(prompt, | |||
}; | |||
} | |||
|
|||
function prettifyStackTrace(err) { | |||
if (err.stack) { | |||
const regex = new RegExp(/\s+repl:/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe new RegExp
is unnecessary in recent ES versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had originally written this inline below as
const idx = stack.findIndex((frame) => /\s+repl:/.test(frame));
Which worked as expected. It just wreaked havoc on my syntax highlighting. 😜 I guess that's not the best reason to use new
. But it's all I've got.
lib/repl.js
Outdated
@@ -209,9 +209,9 @@ function REPLServer(prompt, | |||
// preserve original error for wrapped command | |||
const error = wrappedErr || e; | |||
if (isRecoverableError(error, code)) | |||
err = new Recoverable(error); | |||
err = prettifyStackTrace(new Recoverable(error)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use Error.captureStackTrace()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question. I guess I was just going with something similar to what @princejwesley did in #9601. But perhaps that's not the best approach. I'll try an impl with Error.captureStackTrace()
and push it for review.
@jasnell updated using @bnoordhuis this change might address some of your concerns, since I'm no longer just parsing text but looking at The fragility here is that this breaks if internals change and an anonymous function sneaks in between the time I like the idea of script ids for builtins. It would almost certainly make this implementation a little cleaner. |
lib/repl.js
Outdated
@@ -374,6 +374,18 @@ function REPLServer(prompt, | |||
}; | |||
} | |||
|
|||
Error.prepareStackTrace = function prepareStackTrace(error, structuredStack) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm... I do not think it is a good idea to be extending Error
in this way. Why is the existing Error.captureStackTrace()
not adequate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasnell Error.captureStackTrace()
captures the stack at the point where it is called. This would rarely be the stack a user is looking for, since that call would happen in the internals of REPLServer
, after a call to vm.runInContext()
. I think since it would be after the failed completion of an asynchronous execution, the user stack would be gone.
If I understand the v8 Error API documentation, Error.prepareStackTrace()
is the recommended way to accomplish what I am trying to do.
Edit to add the relevant supporting doc
If you assign a different function value to Error.prepareStackTrace that function will be used to format stack traces. It will be passed the error object that it is preparing a stack trace for and a structured representation of the stack. User stack trace formatters are free to format the stack trace however they want and even return non-string values. It is safe to retain references to the structured stack trace object after a call to prepareStackTrace completes so that it is also a valid return value. Note that the custom prepareStackTrace function is only called once the stack property of Error object is accessed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Lemme stew on this a bit more then :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I too had the feeling that doing this is somewhat wrong but by thinking about it further I agree with @lance that this is probably the right thing to do for the repl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lance User may override Error.prepareStackTrace
in their code.
I suggest something like 👇
const prepareStackTrace = fn => (error, frames) => {
// trim stack frames...
let trimmedFrames = frames;
if (fn) {
// pass trimmed stack frame
error.stack = <<trimmed stack string>>
return fn(error, trimmedFrames);
}
return trimmedFrames;
};
self.outputStream._write = self.outputStream.write;
self.outputStream.write = (d) => {
let pstrace = Error.prepareStackTrace;
Error.prepareStackTrace = prepareStackTrace(pstrace);
self.outputStream._write(d);
Error.prepareStackTrace = pstrace;
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@princejwesley I thought about that as well but that can be seen as a advantage and as a disadvantage at the same time. Some users might want to know where the repl lines come from (probably only node-core and we could work around it but it is still something to consider). Therefore I did not complain about that.
So do we really want to prevent the user from being able to override the function? If he does that in the first place, the user can also filter the internal lines if he cares to do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BridgeAR
May be setting process.stdout.write = process.stdout._write
would disable the trimming feature(because we are binding the original function(outputstream.write
) to same outputstream
object instead of keeping it local to context).
Adding comment is sufficient, right?
Advantages:
- User's choice(like formatting) is preserved.
- repl stack is not leaked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BridgeAR we are not preventing user's printStackTrace
function, Just decorating it with our printStackTrace
which will trim the stack frames and pass it to user defined printStackTrace
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@princejwesley @BridgeAR I am a little conflicted about this, and the example here confuses me a little. What happens when self.outputStream._write(d);
throws? Wouldn't Error.prepareStackTrace
never get restored?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lance wrap it with try/finally block. if self.outputstream._write(d)
throws error, we can't use repl at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two small nits but otherwise LGTM. I can not think of a better way.
test/parallel/test-repl.js
Outdated
@@ -71,7 +71,7 @@ function clean_up() { | |||
function strict_mode_error_test() { | |||
send_expect([ | |||
{ client: client_unix, send: 'ref = 1', | |||
expect: /^ReferenceError:\sref\sis\snot\sdefined\n\s+at\srepl:1:5/ }, | |||
expect: /^ReferenceError:\sref\sis\snot\sdefined\n/ }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the new end of the error? If yes, we might mark that in the RegExp and if not, I would just replace it with the new at
.
|
||
SyntaxError: Unexpected identifier | ||
|
||
` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a huge fan of the multi line template string and would rather see \n
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I disagree. Template strings, to my mind, tend to make the test example easier to scan/read quickly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know this was mainly discouraged so far and the code does not have any multi line template strings. I think it is more difficult to see whitespace with this. I do not want to block the PR with this comment though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm definitely not a fan of multi-line template strings and we've avoided them consistently in the past. It's definitely something that we can revisit. Ping @nodejs/tsc ... any feelings on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's possible to align the first line with the others (maybe with a tagged literal that removes the first line terminator), then I'm +1.
Should this be semver-major though? |
will mark as major just to be safe. Ping @nodejs/tsc |
When a user executes code in the REPLServer which generates an exception, there is no need to display the REPLServer internal stack frames.
@princejwesley @BridgeAR PTAL - updated with a function similar to that suggested. Setting @BridgeAR I can take a look at template strings tomorrow. I'll defer to common wisdom, though I do still stand by my assertion that it's more readable, specifically for test output. |
return fn(error, frames); | ||
} | ||
frames.push(error); | ||
return frames.reverse().join('\n at '); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lance return frames.reverse()
is sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@princejwesley Error.prepareStacktrace()
is used to format the stack trace. It’s not required to return a string, but that is the behavior of the default implementation, and I think it would be unexpected if users suddenly started getting an array from the Error.stack
property.
.replace(/^repl:\d+\r?\n/, '') | ||
.replace(/^\s+at\s.*\n?/gm, ''); | ||
.replace(/^repl:\d+\r?\n/, '') | ||
.replace(/^\s+at\s.*\n?/gm, ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still necessary with the prepareStackTrace
function in place? And if yes, cant we just move that part in the prepareStackTrace function as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can poke around on it more - the change in formatting is because that's what I was doing and missed noticing that on my local diff. It's tricky because of the way Error.prepareStackTrace()
works. It's only ever called once for a given error, and the return value is by default (and typically in custom impls as well) a string. Because of the way errors are handled in the repl and domain, in the case of a SyntaxError
specifically, it's a string before we have an opportunity to filter the stack frames in prepareStackTrace()
. So I left it rather than rework more of the overall error handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BridgeAR I spent the morning looking into removing this and dealing with it in Error.prepareStackTrace()
but the ramifications were wide reaching, especially with regard to how we handle SyntaxError
as a Recoverable
object so that defaulting into editor mode on multi-line input works as expected. I do think there could be some consolidation and overall improvement of the error handling, but I don't think this is the PR to do that in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still LGTM. I do not mind the template string that badly if you feel strongly about it, go ahead.
Multi-line template strings have been removed, and I think I have addressed all other issues. As |
CI: https://ci.nodejs.org/job/node-test-pull-request/10337/ Edit: CI failures appear to be unrelated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with green CI
Do we need another @nodejs/tsc LGTM for this to land? |
Pretty sure CI 2 was green, but didn't comment on it here. Just to be safe, running CI 3. https://ci.nodejs.org/job/node-test-pull-request/10555/ EDIT 1,pi3-raspbian-jessie
|
@jasnell I'd like to land this if possible. Since it's For the record, I'm questioning what makes this a major version bump. What is the scenario where users would be impacted? The only thing I can think of is if the |
I am fine to not have it as semver-major. I just thought it would be good to have some other opinions about it. Too bad there was no response from the @nodejs/tsc about that. |
I don't think this needs to be semver-major |
I'm not certain if any behavior within a repl context can truly be considered semver-major. |
I'm good with semver-patch |
Thanks all for the feedback. I will land this in the next day or so modulo work stuff. |
Landed in c5f54b1 |
When a user executes code in the REPLServer which generates an exception, there is no need to display the REPLServer internal stack frames. PR-URL: #15351 Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Refs: #9601
When a user executes code in the REPLServer which generates an exception, there is no need to display the REPLServer internal stack frames. PR-URL: nodejs/node#15351 Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Refs: nodejs/node#9601
This does not land cleanly on v8.x, would someone be willing to backport? |
@MylesBorins I will take care of it. Edit: PR submitted |
Should this be backported to |
@MylesBorins what determines if it should be backported. The backporting guide doesn't give much in the way of guidelines. I'm happy to do it if it should. |
When a user executes code in the REPLServer which generates an exception, there is no need to display the REPLServer internal stack frames. PR-URL: #15351 Backport-PR-URL: #16457 Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Refs: #9601
@gibfahn @MylesBorins I waited too long and v6.x is in LTS maintenance mode. This, and the fact that the v8 Error API was different enough in The link to the V8 wiki does not adequately describe the behavior I am seeing in my attempts to backport, but it appears that the custom stack formatter is only called if I am going to remove the |
When a user executes code in the REPLServer which generates an
exception, there is no need to display the REPLServer internal
stack frames.
Based on work by @princejwesley.
Should there be an option to display these stack frames for the purposes of Node.js core development, specifically on the REPL itself?
Ref: #9601
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
repl