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.)
should.js
? Ideally, I'd love to see this problem isolated to the point it's reproducable without it (and to be even bolder, in the browser by every reader inside jsfiddle). – Confucius