I spent some time debugging a strange infinite loop problem in a NodeJS testsuite. It only happens under rare conditions but I can reproduce it when I attach to the chrome debugger.
I think it has to do with V8's handling of stack traces in exceptions and a extension that the vows library did to the AssertionError
object (vows added an toString
method). I could also be mistaken, so I wanted to ask whether my understanding of V8's implementation is correct.
Here is a minimal example to reproduce the error:
$ git clone https://github.com/flatiron/vows.git
$ cd vows && npm install && npm install should
$ cat > example.js
var should = require('should');
var error = require('./lib/assert/error.js');
try {
'x'.should.be.json;
} catch (e) {
console.log(e.toString());
}
// without debug, it should fail as expected
$ node example.js
expected 'x' to have property 'headers' // should.js:61
// now with debug
$ node-inspector &
$ node --debug-brk example.js
// 1) open http://127.0.0.1:8080/debug?port=5858 in Chrome
// 2) set breakpoint at lib/assert/error.js#79 (the toString method)
// 3) Resume script execution (F8)
Now the program ends up in an infinite loop: the toString
method (added by the vows library) is called again and again when this.stack
is accessed in the regexp on line 83.
require('assert').AssertionError.prototype.toString = function () {
var that = this, // line 79: breakpoint
source;
if (this.stack) {
source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/); // line 83: infinite loop takes place here (however, this.stack is undefined)
}
When I inspect this
in the debugger, it shows that it is an AssertionError
but its stack
property is undefined
. However, when I hover with the mouse over it, it shows the actual stack trace.
I assume this phenomena is caused by the laziness optimization of V8. It only computes the stack trace on demand. In doing so, it interferes with the added toString
method of vows. The toString
method again accesses the stack trace (this.stack
), so the loop continues.
Is that conclusion correct? If so, is there a way to patch the vows code, so it works with V8 (or can I at least report it as a bug in the vows project)?
I'm using node v0.10.28 under Ubuntu.
Update: Simplified example without vows
Here is a simplified version. It no longer depends on vows, but instead I have only copied the relevant parts of its toString
extension:
var should = require('should');
require('assert').AssertionError.prototype.toString = function () {
var that = this,
source;
if (this.stack) {
source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/);
}
return '<dummy-result>';
};
try {
'x'.should.be.json;
} catch (e) {
console.log(e.toString());
}
// expected result (without debug mode)
$ node example.js
<dummy-result>
In debug mode, the recursion takes place in the if
statement.
Update: Even simplier version with ReferenceError
ReferenceError.prototype.toString = function () {
var that = this,
source;
if (this.stack) {
source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/);
}
return '<dummy-result>';
};
try {
throw new ReferenceError('ABC');
} catch (e) {
console.log(e.toString());
}
(I also create a jsfiddle example, but I cannot reproduce the infinite loop there, only with node.)
Yep, that's definitely a bug in the V8 version in that version of node.
The code in the version of V8 your version of Node uses code that looks something like:
function FormatStackTrace(error, frames) {
var lines = [];
try {
lines.push(error.toString());
} catch (e) {
try {
lines.push("<error: " + e + ">");
} catch (ee) {
lines.push("<error>");
}
}
Here is the actual code from the version NodeJS uses.
The fact it's doing error.toString()
itself is causing the loop, this.stack
accesses FormatStackTrace
which in turn is doing .toString()
. Your observation is correct.
If that's any comfort, that code looks very different in newer versions of V8. In Node 0.11, this bug is already fixed.
Here is the commit that fixed it commited A year and a half ago by Vyacheslav Egorov.
Well, your options are limited, but since this is for debugging anyway, can always prevent the second iteration and stop the loop:
ReferenceError.prototype.toString = function () {
var source;
if(this.alreadyCalled) return "ReferenceError";
this.alreadyCalled = true;
if (this.stack) {
source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/);
}
return '<dummy-result>';
};
Does not exhibit the same issue. There is not much more you could do without access to the core code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With