In my web-app( heavy on client side, light on server side), while in development, I need to do a lot of debugging, console.log is very helpful, but ideally in production, there must not be any debug message shown, so I am planning to add the below code:
window.production = false; // set to true when in production mode.
if(window.production){
window.log = function(){};
}else{
window.log = function(){
console.log.apply(console, arguments);
};
}
//then replace all console.log(a, b, c, ...) into window.log(a, b, c, ...) in my code.
is this a good way to make debugging configurable, or must I just make a grunt that removes all console.log lines for production?
So if your only end goal is to not show debug messages in production you have a huge amount of options to choose from! You should also decide if the following are important to you:
Being able to perform other work outside of your log statement that also should not run in production.
Minimizing file size in production and not shipping logging code.
Multiple log levels, or different logging groups.
The ability to turn these logging levels or groups on/off in production.
Minimizing the amount that you have to type for each log statement.
At a very basic level simply calling
if (window.console && window.console.log)
{
window.log = console.log.bind(console); // function devnull() { };
}
else
{
window.log = function() { };
}
log('This is a log!');
will be enough to allow you to turn logging on/off. This would fulfill goal (5) in the list above and works quite well.
An alternate solution which works well with minifiers like uglify which can remove dead code would be to surround your logging statements with something like (you may not want to pollute the global namespace however):
window.LogLevels =
{
Off: 0x00000000,
Error: 0x00000001,
Warning: 0x00000002,
Timing: 0x00000004,
Data: 0x00000008,
Status: 0x00000010,
...
Verbose: 0x04000000,
};
window.LogLevel = LogLevels.Error | LogLevels.Warning;
window.ShouldLog = function(mask)
{
return ((window.LogLevel & mask) === mask);
};
if (ShouldLog(LogLEvels.Error)) { log('This is an error!'); }
This would satisfy condition (1), (3), and (4) and sets you up to solve (2) as well at the cost of (5).
Coupled with a pre-defined DEBUG constant (or similar), in your build step you can replace the log statements with a regex:
productionCode = debugCode.replace(/ShouldLog\(((?!LogLevels\.Error|LogLevels\.Warning)[^)]*)\)/g, 'DEBUG');
This would completely remove non-error and non-warning level logging in your code and satisfy (2). You don't really want people peeking at your logs right.. plus better perf! :)
Bonus
If you want to get extra funky, you can use the following (at least in Chrome) to get a stack trace for each logging statement in your console. No more 'why did this log get hit'!
window.log = function ()
{
console.groupCollapsed.apply(console, arguments);
var stack = new Error().stack.split('\n');
for(var i = 2; i < stack.length; i ++)
{
// Trim and remove 'at ';
console.log('%c' + stack[i].trim().substring(3), 'padding-left: 10px; color: #777');
}
console.groupEnd();
};
If you are using build tools like Gulp.js or Grunt.js, then there are modules to do that :
gulp strip debug
grunt strip debug
Related
In my current project with lots of dependencies I need a way to disable console access for specific libraries so that those files can't use any of the console functionality.
I could of course disable console functionality by simply finding and replacing it in the library bundle, but as this project has a lot of dependencies that would make updating libraries a huge hassle.
I'm aware that I can disable console functionality by overwriting it with an empty function block:
console.log = function(){};
But that disables the console functionality for the entire project. So im looking for an implementation, or a line of code with which I can disable console functionality for a specific file or code block.
Write a white-listing "middleware" for console.log
// Preserve the old console.log
const log = console.log;
// Used a dictionary because it's faster than lists for lookups
const whiteListedFunctions = {"hello": true};
// Whitelisting "middleware". We used the function's name "funcName"
// as a criteria, but it's adaptable
const isWhitelisted = callerData => callerData.funcName in whiteListedFunctions;
// Replacing the default "console.log"
console.log = arg => {
const stack = new Error().stack.split("at")[2].trim().split(' ');
const fileParts = stack[1].substr(1, stack[1].length - 2).split(':');
const callerData = {
funcName: stack[0],
file: fileParts.slice(0, fileParts.length - 2).join(':'),
lineColNumber: fileParts.slice(fileParts.length - 2).join(':')
};
if (isWhitelisted(callerData)) { // Filtering happens here
log(arg);
}
};
// Define the calling functions
function hello() { console.log("hello"); }
function world() { console.log("world"); }
hello(); // => Prints hello
world(); // => Doesn't print anything
Method explanation
You can do this by creating a whitelist (or blacklist) that will contain your filtering criteria. For example it may contain the name of the functions that call console.log or maybe the file name, or even the line and column numbers.
After that you create your whitelisting "middleware". This will take the caller function data and decide if it can log stuff or not. This will be done based on the previously defined whitelist. You can choose your preferred criteria in this "middleware".
Then you actually replace console.log by overriding with your new logger. This logger will take as an argument the message to log (maybe multiple arguments?). In this function you also need to find the data relating to the caller function (which wanted to call console.log).
Once you have the caller data, you can then use your whitelisting middleware to decide if it can log stuff
Getting information about the caller function
This part is a little "hacky" (but it got the job done in this case). We basically create an Error and check its stack attribute like this new Error().stack. Which will give us this trace
Error
at console.log.arg [as log] (https://stacksnippets.net/js:25:7)
at hello (https://stacksnippets.net/js:41:11)
at https://stacksnippets.net/js:48:1
After processing (split, map, etc...) the trace we get the caller function data. For example here we have
The caller function's name: hello
The file name: https://stacksnippets.net/js
The line and column number: 41:11 (watch out for minifiers)
This bit was inspired by VLAZ's answer in How to disable console.log messages based on criteria from specific javascript source (method, file) or message contents, so make sure to check it out. Really good and thorough post.
Note
To make sense of the trace we can do new Error().stack.split("at")[INDEX].trim().split(' ') where INDEX is the position of the function call you want to target in the stack trace. So if you want to get a different "level" that the one used in this example, try changing INDEX
Just redefine the console to log over a condition, your condition of course will be a check over which library is accessing the function:
// Your condition, could be anything
let condition = true;
/* Redefine the console object changing only the log function with your new version and keeping all the other functionalities intact
*/
let console = (old => ({
...old,
log: text => { if (condition) old.log(text) }
}))(window.console)
// Redefine the old console
window.console = console;
console.log('hello!')
Hope it helped :)
Yes, you can disable console logs from files based on their path! Here's a solution:
// in ./loud-lib.js
module.exports = {
logsSomething: () => console.log('hello from loud-lib')
}
// in ./silent-lib.js
module.exports = {
logsSomething: () => console.log('hello from silent-lib')
}
// in ./index.js
const loud = require('./loud-lib');
const silent = require('./silent-lib');
// save console.log
const log = console.log;
// redefinition of console.log
console.log = (...params) => {
// define regexp for path of libraries that log too much
const loudLibs = [/loud-lib/];
// check if the paths logged in the stacktract match with at least one regexp
const tooLoud = !!loudLibs.find(reg => reg.test(new Error().stack));
// log only if the log is coming from a library that doesn't logs too much
if (!tooLoud) log(...params);
};
loud.logsSomething();
silent.logsSomething();
$ node ./index.js
hello from silent-lib
This is based on the fact that new Error() produces a stack trace that identifies from which file is the error coming from (recursively).
Based on this observation, you can define an array of regular expression that match the name of libraries you don't want to hear logs from. You can get really specific and creative with the re-definition of console.log, but I kept it simple.
However, be aware of this (especially when using Webpack): if you bundle all your JS assets into one single bundle.js, the stacktrace will always point to bundle.js, thus logging everything. You'll have to go further from my code, for example by using stack-source-map, but I don't have sufficient details on your project to deliver a solution. I hope the ideas above are sufficient for you.
I am working on project that uses quite a few js libraries and one of them is outputting awful lot into console, it is polluting the airwaves so bad that it makes it hard to debug....
I know how to disable logging completely by overriding console.log with this,
(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);
but how do it do that per source(file/url) of where message originated?
Preamble
The beginning discusses how stuff works in general. If you just care for the code, skip Introduction and scroll to the Solution heading.
Introduction
Problem:
there is a lot of console noise in a web application. A significant amount of that noise is coming from third party code which we do not have access to. Some of the log noise might be coming from our code, as well.
Requirement:
reduce the noise by stopping the log. Some logs should still be kept and the decision about those should be decoupled from the code that is doing the logging. The granularity needed is "per-file". We should be able to choose which files do or do not add log messages. Finally, this will not be used in production code.
Assumption: this will be ran in a developer controlled browser. In that case, I will not focus on backwards compatibility.
Prior work:
First off logging can be enabled/disabled globally using this
(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);
(code posted in the question but also here for reference)
However, that does not allow for any granularity.
This could be modified to work on only specific modules but that cannot be done for third party code.
A mixed approach would be to disable logging globally but enable it in each of our modules. Problem there is that we have to modify each of our files and we will not get some potentially useful external messages.
A logging framework can be used but it might be an overkill. Although, to be honest, that's what I'd go for, I think, but it may need some integration into the product.
So, we need something light-weight-ish that has some configuration and does not need to be pretty.
Proposal:
The Loginator (title subject to change)
Let's start with the basics - we already know we can override the global log function. We'll take that and work with it. But first, let's recognise that the console object supports more than just .log. There could be various logging functions used. So-o-o, let's disable all of them.
Silence everything
//shorthand for further code.
function noop() {}
const savedFunctions = Object.keys(console)
.reduce((memo, key) => {
if(typeof console[key] == "function") {
//keep a copy just in case we need it
memo[key] = console[key];
//de-fang any functions
console[key] = noop;
}
return memo;
},
{});
console.log("Hello?");
console.info("Hello-o-o-o?");
console.warn("Can anybody hear me?");
console.error("I guess there is nobody there...");
savedFunctions.log("MUAHAHAHA!")
This can obviously be improved but it showcases how any and ll logging can be stopped. In reality, console.error should probably be left and console.warn might be also useful. But this is not the be-all-and-end-all solution.
Next, since we can override console functionality...why not supply our own?
Custom logging
const originalLog = console.log;
console.log = function selectiveHearing() {
if (arguments[0].indexOf("die") !== -1) {
arguments[0] = "Have a nice day!";
}
return originalLog.apply(console, arguments)
}
console.log("Hello.");
console.log("My name is Inigo Montoya.");
console.log("You killed my father.");
console.log("Prepare to die.");
That is all the tools we need to roll our own mini-logging framework.
How to do selective logging
The only thing missing is to determine which file something is coming from. We just need a stack trace.
// The magic
console.log(new Error().stack);
/* SAMPLE:
Error
at Object.module.exports.request (/home/vagrant/src/kumascript/lib/kumascript/caching.js:366:17)
at attempt (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:180:24)
at ks_utils.Class.get (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:194:9)
at /home/vagrant/src/kumascript/lib/kumascript/macros.js:282:24
at /home/vagrant/src/kumascript/node_modules/async/lib/async.js:118:13
at Array.forEach (native)
at _each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:39:24)
at Object.async.each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:117:9)
at ks_utils.Class.reloadTemplates (/home/vagrant/src/kumascript/lib/kumascript/macros.js:281:19)
at ks_utils.Class.process (/home/vagrant/src/kumascript/lib/kumascript/macros.js:217:15)
*/
(Relevant bit copied here.)
True, there are some better ways to do it but not a lot. It would either require a framework or it's browser specific - error stacks are not officially supported but they work in Chrome, Edge, and Firefox. Also, come on - it's literally one line - we want simple and don't mind dirty, so I'm happy for the tradeoff.
Solution
Putting it all together. Warning: Do NOT use this in production
(function(whitelist = [], functionsToPreserve = ["error"]) {
function noop() {}
//ensure we KNOW that there is a log function here, just in case
const savedFunctions = { log: console.log }
//proceed with nuking the rest of the chattiness away
Object.keys(console)
.reduce((memo, key) => {
if(typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1 ) {
memo[key] = console[key];
console[key] = noop;
}
return memo;
},
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];
if (whitelist.some(entry => callingFile.includes(entry))) {
savedFunctions.log.apply(console, arguments)
}
}
})(["myFile.js"]) //hey, it's SOMEWHAT configurable
Or a blacklist
(function(blacklist = [], functionsToPreserve = ["error"]) {
function noop() {}
//ensure we KNOW that there is a log function here, just in case
const savedFunctions = {
log: console.log
}
//proceed with nuking the rest of the chattiness away
Object.keys(console)
.reduce((memo, key) => {
if (typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1) {
memo[key] = console[key];
console[key] = noop;
}
return memo;
},
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];
if (blacklist.some(entry => callingFile.includes(entry))) {
return;
} else {
savedFunctions.log.apply(console, arguments);
}
}
})(["myFile.js"])
So, this is a custom logger. Sure, it's not perfect but it will do the job. And, hey, since the whitelisting is a bit loose, it could be turned to an advantage:
to whitelist a bunch of files that share a substring, say, all myApp can include myApp1.js, myApp2.js, and myApp3.js.
although if you want specific files, you can just pass the full name, including extension. I doubt there would be a bunch of duplicate filenames.
finally, the stack trace will include the name of the calling function, if any, so you can actually just pass that and that will whitelist on per-function basis. However, it relies on the function having a name and it's more likely for function names to clash, so use with care
Other than that, there can certainly be improvements but that is the basis of it. The info/warn methods can also be overriden, for example.
So, this, if used, should only be in dev builds. There are a lot of ways to make it not go into production, so I won't discuss them but here is one thing I can mention: you can also use this anywhere if you save it as a bookmarklet
javascript:!function(){function c(){}var a=arguments.length<=0||void 0===arguments[0]?[]:arguments[0],b=arguments.length<=1||void 0===arguments[1]?["error"]:arguments[1],d={log:console.log};Object.keys(console).reduce(function(a,d){return"function"==typeof console[d]&&b.indexOf(d)!=-1&&(a[d]=console[d],console[d]=c),a},d),console.log=function(){var c=(new Error).stack.split("\n")[2];a.some(function(a){return c.includes(a)})&&d.log.apply(console,arguments)}}(["myFile.js"]);
This is it minified (although I passed it through Babel first, to use ES5 minification) and still configurable, to an extent, as you can change the very end where you can pass the whitelist. But other than that, it will work the same and is completely decoupled from the codebase. It will not run at pageload but if that's needed you can either use this as a userscript (still decoupled) or include it before other JS files in dev/debug builds only.
A note here - this will work in Chrome, Edge and Firefox. It's all the latest browsers, so I assume a developer will use at least one of them. The question is tagged as Chrome but I decided to widen the support. A Chrome only solution could work slightly better but it's not really a big loss of functionality.
I was as troubled as you. This is my approach. https://github.com/jchnxu/guard-with-debug
Simple usage:
localStorage.debug = [
'enable/console/log/in/this/file.ts',
'enable/console/log/in/this/folder/*',
'-disable/console/log/in/this/file.ts',
'-disable/console/log/in/this/folder/*',
// enable all
'*',
].join(',');
The benefit: it's zero-runtime.
Disclaimer: I am the author of this tiny utility
It work in chrome:
...index.html
<html>
<body>
<script>
(function(){
var original = console.log;
console.log = function(){
var script = document.currentScript;
alert(script.src);
if(script.src === 'file:///C:/Users/degr/Desktop/script.js') {
original.apply(console, arguments)
}
}
})();
console.log('this will be hidden');
</script>
<script src="script.js"></script>
</body>
</html>
...script.js
console.log('this will work');
Console.log does not work from index.html, but work from script.js. Both files situated on my desctop.
I've found these settings in the latest (July 2020) Chrome DevTools console to be helpful:
DevTools | Console | (sidebar icon) | user messages
DevTools | Console | (gear icon) | Select context only
DevTools | Console | (gear icon) | Hide network
I like (1) most, I only see the messages from "my" code. (2) hides messages from my iframe.
If it's an option to modify file, you can set a flag at top of file for disabling logs for that:
var DEBUG = false;
DEBUG && console.log("cyberpunk 2077");
To disable logs for all js files, put it once at top of any js file:
var DEBUG = false;
if (!DEBUG) {
console.log = () => {};
}
This is not pretty but will work.
Put something like this in your file before the <script> tag of the "bad" library :
<script>function GetFile(JSFile) {
var MReq = new XMLHttpRequest();
MReq.open('GET', JSFile, false);
MReq.send();
eval(MReq.responseText.replace(/console.log\(/g,"(function(){})("));
}</script>
Then replace the tag
<script src="badLib.js">
With:
GetFile("badLib.js")
Only for short time debugging.
My [research] program is growing quite large, like many projects. I also battle the fight between getting work done and teaching. I would like to have different amounts of console.log() statements with just changing something like a global variable or similar. This is similar to typing --v (or similar for VERBOSE) in some terminal/console commands to get extra help of what is going on during a process. This way for normal working, I can have the level be Normal, but when I let a student come onto a project, I can have them set the level to Verbose so they can see what is going on to help catch up, as not all the students are familiar with JS and its humors :)
JS already has console.log(),console.warn(), and console.error() for different message intensities in code, but I am looking for something that would be similar to normal.console.log(), 30ThousandFootHighLevel.console.log(), and giveMeEverythingYouGot.console.log() so I can include the appropriate levels throughout the application. I would then, in some global scope like place, would say something like consoleLogLevel = 3; // 0-N with N being the most verbose.
BONUS points if I don't have to write each log level , but rather when giveMeEverythingYouGot.console.log() shows all [lower/inclusive/previous] levels.
Any node packages? hard code examples?
Here is a basic PLNKR set up to help with suggestions. THANKS!
var consoleLogArray = ['NORMAL','MIDDLE','VERBOSE'];
var consoleLogLevel = 3;
$(document).ready( function(){
// Got to let me know we are starting in some cases
console.log('VERBOSE MERSSAGE: Started');
// Some Basic code
var x = 5;
var y = 10;
var check = 2;
if (!x+y === 14){
// This should be a verbose log
// This should only show in Verbose setting
console.error('We have a variable assignment problem')
}
if(x/y === check){
// This should be a middle level
// This should only show in Verbose setting or Middle Setting
console.log('Yup, 10 divided by 5 is still 2...')
}
// This should be a normal level
// This should show in Normal, Middle, and Verbose settings
console.log('Finished with no errors!');
})
I don't understand how you assign verbose, middle, and normal to those log messages. But I'm going to assume you know when you write the log statement which level you want it to be.
So, how about this?
log = {
order: {
"verbose": 0,
"debug": 1,
"warn": 2
},
log_level: "warn",
verbose: function (arguments) {
if (this.order[this.log_level] <= this.order["verbose"]) {
console.log(arguments);
}
},
debug: function (arguments) {
if (this.order[this.log_level] <= this.order["debug"]) {
console.log(arguments);
}
},
warn: function (arguments) {
if (this.order[this.log_level] <= this.order["warn"]) {
console.log(arguments);
}
}
}
log.verbose("This is verbose"); // doesn't print
log.debug("This is debug"); // doesn't print
log.warn("This is warn"); // prints
log.log_level = "verbose"; // change what level of messages we want
log.verbose("This is verbose"); // prints
log.debug("This is debug"); // prints
log.warn("This is warn"); // prints
This way log statements are semantic. To send a verbose log you call a function by that name: log.verbose. You can add arbitrarily many levels by extending the order object.
The functions for each logging function (verbose, debug, warn) are almost identical. I'm sure you can write some higher-level function that generates those functions. Let's leave that as an exercise for the reader. :)
Performance is important for a certain class I'm writing.
I thought about calling a function like so:
debug('This is a debug message, only visible when debugging is on');
And the contents would be like
function debug(message) {
if (DEBUG) console.log(message);
}
So I wonder: is this enough for V8 to flag this as "dead code" if the DEBUG variable never changes?
Edit: I'm more worried about the performance in Node than on the browser, so removing the code while minifying would be insufficient.
Edit2: I made a JSPerf benchmark out of the proposed solutions, and they are very surprising: http://jsperf.com/verbose-debug-loggin-conditionals-functions-and-no-ops/3
I use comments that when a file is minified get removed, such as:
function name(arg) {
// <debug>
if (!arg) {
throw new Error("arg must be defined");
}
// </debug>
... code goes here
}
For example: https://github.com/PANmedia/raptor-editor/blob/master/src/raptor-widget.js#L29-L33
An my (custom) build script to do the aforementioned https://github.com/PANmedia/raptor-build/blob/master/build/raptor-builder.js#L305-L313
There's a couple of solutions available(aside Petah's...):
Use UglifyJS2 conditional compilation:
You can use the --define (-d) switch in order to declare global
variables that UglifyJS will assume to be constants (unless defined in
scope). For example if you pass --define DEBUG=false then, coupled
with dead code removal UglifyJS will discard the following from the
output:
if (DEBUG) {
console.log("debug stuff");
}
UglifyJS will warn about the condition being always false and about
dropping unreachable code; for now there is no option to turn off only
this specific warning, you can pass warnings=false to turn off all
warnings.
Another way of doing that is to declare your globals as constants in a
separate file and include it into the build. For example you can have
a build/defines.js file with the following:
const DEBUG = false;
const PRODUCTION = true;
// etc.
and build your code like this:
uglifyjs build/defines.js js/foo.js js/bar.js... -c UglifyJS will
notice the constants and, since they cannot be altered, it will
evaluate references to them to the value itself and drop unreachable
code as usual. The possible downside of this approach is that the
build will contain the const declarations.
Use a wrapper function.
For example you have this method:
exports.complicatedMethod = function (arg1, arg2, arg3) {
stuff...
};
You add logging to it by wrapping it in a logger function:
function logger(fn) {
if (!DEBUG) {
return fn;
}
return function () {
console.log(fn.name, arguments); // You can also use `fn.toString()` to get the argument names.
fn.apply(this, arguments);
};
}
exports.complicatedMethod = logger(function (arg1, arg2, arg3) {
stuff...
});
This way the only performance hit would be at startup time. You can also use AOP method with the above wrapper function:
exports.complicatedMethod = function (arg1, arg2, arg3) {
stuff...
};
if (DEBUG) {
for (var name in exports) {
exports[name] = logger(exports[name]);
}
}
And you can pass information to the logger by adding properties to the function:
exports.complicatedMethod.description = 'This function only shows how tired i was when I was writing it and nothing else!';
You can have a look at this question where someone created code that creates a logger for functions in an object recursively. Also check this answer of mine.
Use C Pre Processor.
You can just do something like this:
#if DEBUG
console.log("trace message");
#endif
or something like this
#if DEBUG
#define DEBUG_LOG(x) console.log(x);
#else
#define DEBUG_LOG(x) //console.log(x);
#endif
Then you can do this in your code
DEBUG_LOG('put a random message her to confuse sys admins!')
Or you use it's npm warapper: laudanumscript
Create a sweetjs macro.
I haven't been able to find conditional compilation with sweetjs, but I'm sure it wouldn't be too hard to implement it. The end syntax would be(or should be!) similar to cpp.
You can use a logger library that supports:
Logging level
Late binding functions
Example: https://github.com/duongchienthang/js-logger
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.)
Congratulations, you have found a bug in V8!
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.
What you can do about it?
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.