When creating new function from JavaScript code using new Function(params,body) constructor, passing invalid string in body yelds SyntaxError. While this exception contains error message (ie: Unexpected token =), but does not seem to contain context (ie. line/column or character where error was found).
Example fiddle: https://jsfiddle.net/gheh1m8p/
var testWithSyntaxError = "{\n\n\n=2;}";
try {
var f=new Function('',testWithSyntaxError);
} catch(e) {
console.log(e instanceof SyntaxError);
console.log(e.message);
console.log(e.name);
console.log(e.fileName);
console.log(e.lineNumber);
console.log(e.columnNumber);
console.log(e.stack);
}
Output:
true
(index):54 Unexpected token =
(index):55 SyntaxError
(index):56 undefined
(index):57 undefined
(index):58 undefined
(index):59 SyntaxError: Unexpected token =
at Function (native)
at window.onload (https://fiddle.jshell.net/_display/:51:8)
How can I, without using external dependencies, pinpoint SyntaxError location withinn passed string? I require solution both for browser and nodejs.
Please note: I do have a valid reason to use eval-equivalent code.
In Chromium-based browsers, as you've seen, putting try/catch around something that throws a SyntaxError while V8 is parsing the code (before actually running it) won't produce anything helpful; it will describe the line that caused the evaluation of the problematic script in the stack trace, but no details on where the problem was in said script.
But, there's a cross-browser workaround. Instead of using try/catch, you can add an error listener to window, and the first argument provided to the callback will be an ErrorEvent which has useful lineno and colno properties:
window.addEventListener('error', (errorEvent) => {
const { lineno, colno } = errorEvent;
console.log(`Error thrown at: ${lineno}:${colno}`);
// Don't pollute the console with additional info:
errorEvent.preventDefault();
});
const checkSyntax = (str) => {
// Using setTimeout because when an error is thrown without a catch,
// even if the error listener calls preventDefault(),
// the current thread will stop
setTimeout(() => {
eval(str);
});
};
checkSyntax(`console.log('foo') bar baz`);
checkSyntax(`foo bar baz`);
Look in your browser console to see this in action, not in the snippet console
Check the results in your browser console:
Error thrown at: 1:20
Error thrown at: 1:5
Which is what we want! Character 20 corresponds to
console.log('foo') bar baz
^
and character 5 corresponds to
foo bar baz
^
There are a couple issues, though: it would be good to make sure in the error listened for is an error thrown when running checkSyntax. Also, try/catch can be used for runtime errors (including syntax errors) after the script text has been parsed into an AST by the interpreter. So, you might have checkSyntax only check that the Javascript is initially parsable, and nothing else, and then use try/catch (if you want to run the code for real) to catch runtime errors. You can do this by inserting throw new Error to the top of the text that's evaled.
Here's a convenient Promise-based function which can accomplish that:
// Use an IIFE to keep from polluting the global scope
(async () => {
let stringToEval;
let checkSyntaxResolve;
const cleanup = () => {
stringToEval = null;
checkSyntaxResolve = null; // not necessary, but makes things clearer
};
window.addEventListener('error', (errorEvent) => {
if (!stringToEval) {
// The error was caused by something other than the checkSyntax function below; ignore it
return;
}
const stringToEvalToPrint = stringToEval.split('\n').slice(1).join('\n');
// Don't pollute the console with additional info:
errorEvent.preventDefault();
if (errorEvent.message === 'Uncaught Error: Parsing successful!') {
console.log(`Parsing successful for: ${stringToEvalToPrint}`);
checkSyntaxResolve();
cleanup();
return;
}
const { lineno, colno } = errorEvent;
console.log(`Syntax error thrown at: ${lineno - 1}:${colno}`);
console.log(describeError(stringToEval, lineno, colno));
// checkSyntaxResolve should *always* be defined at this point - checkSyntax's eval was just called (synchronously)
checkSyntaxResolve();
cleanup();
});
const checkSyntax = (str) => {
console.log('----------------------------------------');
return new Promise((resolve) => {
checkSyntaxResolve = resolve;
// Using setTimeout because when an error is thrown without a catch,
// even if the 'error' listener calls preventDefault(),
// the current thread will stop
setTimeout(() => {
// If we only want to check the syntax for initial parsing validity,
// but not run the code for real, throw an error at the top:
stringToEval = `throw new Error('Parsing successful!');\n${str}`;
eval(stringToEval);
});
});
};
const describeError = (stringToEval, lineno, colno) => {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
};
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
})();
Look in your browser console to see this in action, not in the snippet console
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
Result:
----------------------------------------
Syntax error thrown at: 1:29
console.log('I will throw') bar baz
^
----------------------------------------
Syntax error thrown at: 1:5
foo bar baz will throw too
^
----------------------------------------
Parsing successful for: console.log('A snippet without compile errors'); const foo = bar;
----------------------------------------
Syntax error thrown at: 2:6
With a syntax error on the second line
^
If the fact that an error is thrown at window is a problem (for example, if something else is already listening for window errors, which you don't want to disturb, and you can't attach your listener first and call stopImmediatePropagation() on the event), another option is to use a web worker instead, which has its own execution context completely separate from the original window:
// Worker:
const getErrorEvent = (() => {
const workerFn = () => {
const doEvalAndReply = (jsText) => {
self.addEventListener(
'error',
(errorEvent) => {
// Don't pollute the browser console:
errorEvent.preventDefault();
// The properties we want are actually getters on the prototype;
// they won't be retrieved when just stringifying
// so, extract them manually, and put them into a new object:
const { lineno, colno, message } = errorEvent;
const plainErrorEventObj = { lineno, colno, message };
self.postMessage(JSON.stringify(plainErrorEventObj));
},
{ once: true }
);
eval(jsText);
};
self.addEventListener('message', (e) => {
doEvalAndReply(e.data);
});
};
const blob = new Blob(
[ `(${workerFn})();`],
{ type: "text/javascript" }
);
const worker = new Worker(window.URL.createObjectURL(blob));
// Use a queue to ensure processNext only calls the worker once the worker is idle
const processingQueue = [];
let processing = false;
const processNext = () => {
processing = true;
const { resolve, jsText } = processingQueue.shift();
worker.addEventListener(
'message',
({ data }) => {
resolve(JSON.parse(data));
if (processingQueue.length) {
processNext();
} else {
processing = false;
}
},
{ once: true }
);
worker.postMessage(jsText);
};
return (jsText) => new Promise((resolve) => {
processingQueue.push({ resolve, jsText });
if (!processing) {
processNext();
}
});
})();
// Calls worker:
(async () => {
const checkSyntax = async (str) => {
console.log('----------------------------------------');
const stringToEval = `throw new Error('Parsing successful!');\n${str}`;
const { lineno, colno, message } = await getErrorEvent(stringToEval);
if (message === 'Uncaught Error: Parsing successful!') {
console.log(`Parsing successful for: ${str}`);
return;
}
console.log(`Syntax error thrown at: ${lineno - 1}:${colno}`);
console.log(describeError(stringToEval, lineno, colno));
};
const describeError = (stringToEval, lineno, colno) => {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
};
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
})();
Look in your browser console to see this in action, not in the snippet console
Essentially, what checkSyntax is doing is checking to see if the code provided can be parsed into an Abstract Syntax Tree by the current interpreter. You can also use packages like #babel/parser or acorn to attempt to parse the string, though you'll have to configure it for the syntax permitted in the current environment (which will change as new syntax gets added to the language).
const checkSyntax = (str) => {
try {
acorn.Parser.parse(str);
console.log('Parsing successful');
} catch(e){
console.error(e.message);
}
};
checkSyntax(`console.log('I will throw') bar baz`);
checkSyntax(`foo bar baz will throw too`);
checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
<script src="https://cdn.jsdelivr.net/npm/acorn#6.1.1/dist/acorn.min.js"></script>
The above works for browsers. In Node, the situation is different: listening for an uncaughtException can't be used to intercept the details of syntax errors, AFAIK. However, you can use vm module to attempt to compile the code, and if it throws a SyntaxError before running, you'll see something like this. Running
console.log('I will throw') bar baz
results in a stack of
evalmachine.<anonymous>:1
console.log('I will throw') bar baz
^^^
SyntaxError: Unexpected identifier
at createScript (vm.js:80:10)
at Object.runInNewContext (vm.js:135:10)
<etc>
So, just look at the first item in the stack to get the line number, and at the number of spaces before the ^ to get the column number. Using a similar technique as earlier, throw an error on the first line if parsing is successful:
const vm = require('vm');
const checkSyntax = (code) => {
console.log('---------------------------');
try {
vm.runInNewContext(`throw new Error();\n${code}`);
}
catch (e) {
describeError(e.stack);
}
};
const describeError = (stack) => {
const match = stack
.match(/^\D+(\d+)\n(.+\n( *)\^+)\n\n(SyntaxError.+)/);
if (!match) {
console.log('Parse successful!');
return;
}
const [, linenoPlusOne, caretString, colSpaces, message] = match;
const lineno = linenoPlusOne - 1;
const colno = colSpaces.length + 1;
console.log(`${lineno}:${colno}: ${message}\n${caretString}`);
};
checkSyntax(`console.log('I will throw') bar baz`);
checkSyntax(`foo bar baz will throw too`);
checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
Result:
---------------------------
1:29: SyntaxError: Unexpected identifier
console.log('I will throw') bar baz
^^^
---------------------------
1:5: SyntaxError: Unexpected identifier
foo bar baz will throw too
^^^
---------------------------
Parse successful!
---------------------------
2:6: SyntaxError: Unexpected identifier
With a syntax error on the second line
^
That said:
How can I, without using external dependencies, pinpoint SyntaxError location withinn passed string? I require solution both for browser and nodejs.
Unless you have to achieve this without an external library, using a library really would be the easiest (and tried-and-tested) solution. Acorn, as shown earlier (and other parsers) work in Node as well.
I'm sumarizing comments and some additional research:
Simple anwer: currently impossible
There is currently no cross-platform way to retrive syntax error position from new Function() or eval() call.
Partial solutions
Firefox support non-standard properties error.lineNumber and error.e.columnNumber. This can be used with feature detection if position of error is not critical.
There are filled bug reports/feature request for v8 that could bring support of (1) to chrome/node.js: Issue #1281, #1914, #2589
Use separate javascript parser, based on JSLint or PEG.js.
Write custom javascript parser for the job.
Solutions 1 and 2 are incomplete, rely on features that are not part of standard. They can be suitable if this information is a help, not an requirement.
Solution 3 depends on external codebase, which was explicitly required by original question. It is suitable if this information is required and larger codebase is acceptable compromise.
Solution 4 is impractical.
Credits: #user3896470, #ivan-kuckir, #aprillion
Solution for browser:
You can use latest Firefox to get the required information like error line number and column number inside the string.
Example:
var testWithSyntaxError = "{\n\n\n\nvar x=3;\n =2;}";
try {
var f=new Function('',testWithSyntaxError);
} catch(e) {
console.log(e instanceof SyntaxError);
console.log(e.message);
console.log(e.name);
console.log(e.fileName);
console.log(e.lineNumber);
console.log(e.columnNumber);
console.log(e.stack);
}
Output in Firefox console:
undefined
true
expected expression, got '='
SyntaxError
debugger eval code
6
1
#debugger eval code:4:11
Where 6 is the line number and 1 is the column number of error inside the string.
It won't work in Chrome. There are bugs filed regarding this issue for chrome browser. See:
https://bugs.chromium.org/p/v8/issues/detail?id=1281
https://bugs.chromium.org/p/v8/issues/detail?id=1914
https://bugs.chromium.org/p/v8/issues/detail?id=2589
Related
I am getting the dreaded Cannot access 'server' before initialization error in code that is identical to code that's running in production.
The only things that have changed are my OS version (macOS 10.11->10.14) my NodeJS version (10->12) and my VSCode launch.json, but I cannot see anything in either that would cause an issue. My Node version went from 10 to 12, but in production it went from 8 to 15 without issue. I routinely keep launch.json pretty sparse, and the same error happens using node server in Terminal.
Here is the offending code. The issue occurs because I have shutdown() defined before server and it references server. It's written to add an event-handler and then cause the event. Yes, it could be refactored but it already works. It works, really. In 21 instances spread over 7 servers.
I have tried changing the declaraion/init of server from const to var but that does not fix it. As mentioned, this is code that's running in prod! What's wrong with my environment?
Maybe a better question is: why did this ever work?
'use strict'
const fs = require('fs');
const https = require('https');
const cyp = require('crypto').constants;
const stoppable = require('./common/stoppable.js');
const hu = require('./common/hostutil');
process.on('uncaughtException', err => {
wslog.error(`Uncaught Exception: ${err} ${err.stack}`);
shutdown();
});
process.on('unhandledRejection', (reason, p) => {
wslog.error(`Unhandled Promise Rejection: ${reason} - ${p}`);
});
// 'shutdown' is a known static string sent from node-windows wrapper.js if the service is stopped
process.on('message', m => {
if (m == 'shutdown') {
wslog.info(`${wsconfig.appName} has received shutdown message`);
shutdown();
}
});
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('SIGHUP', shutdown);
function shutdown() {
httpStatus = 503; // Unavailable
wslog.info(`${wsconfig.appName} httpStatus now ${httpStatus} - stopping server...`);
// Error happens on this next line; It should not execute till after server is running already
server.on('close', function () {
wslog.info(`${wsconfig.appName} HTTP server has stopped, now exiting process.`);
process.exit(0)
});
server.stop();
}
// Init and start the web server/listener
var combiCertFile = fs.readFileSync(wsconfig.keyFile, 'utf8');
var certAuthorityFile = fs.readFileSync(wsconfig.caFile, 'utf8');
var serverOptions = {
key: combiCertFile,
cert: combiCertFile,
ca: certAuthorityFile,
passphrase: wsconfig.certPass,
secureOptions: cyp.SSL_OP_NO_TLSv1 | cyp.SSL_OP_NO_TLSv1_1
};
var server = https.createServer(serverOptions, global.app)
.listen(wsconfig.port, function () {
wslog.info(`listening on port ${wsconfig.port}.`);
});
server.on('clientError', (err, socket) => {
if (err.code === 'ECONNRESET' || !socket.writable) { return; }
// ECONNRESET was already logged in socket.on.error. Here, we log others.
wslog.warn(`Client error: ${err} ${err.stack}`);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.on('error', (err)=>{
if ( err.code === 'EADDRINUSE' ) {
wslog.error(`${err.code} FATAL - Another ${wsconfig.appName} or app is using my port! ${wsconfig.port}`);
} else {
wslog.error(`${err.code} FATAL - Server error: ${err.stack}`);
}
shutdown();
})
combiCertFile = null;
certAuthorityFile = null;
// Post-instantiation configuration required (may differ between apps: need an indirect way to plug in app-specific behavior)
stoppable(server, wsconfig.stopTimeout);
// Load all RESTful endpoints
const routes = require('./routes/');
This is a runtime error, which happens only in a very specific situation. But actually this exact error shouldn't happen with var server = ... but only with const server = ... or let server = .... With var server = ... the error message should say "Cannot read properties of undefined"
What happens
You have an error handler for uncaughtException which is calling shutdown() and in shutdown() you are referencing your server. But consider what happens if your code throws an exception before you initialized your server. For instance if your cert or key cannot be read from the disk, cert or key are invalid ... So nothing will be assigned to server, and an exception will be raised.
Then the handler for your uncaught exception will fire and call the shutdown() function, which then tries to access the server, which of course hasn't been initialized yet.
How to fix
Check what the unhandled exception is, that is thrown before your server is initialized and fix it. In your production environment, there is probably no exception, because the configuration and environment is properly set up. But there is at least one issue in your develepment environment, which causes an exception.
Difference between var and const
And the difference between var server = ... and const server = ... is quite a subtle one. For both, the declaration of the variable is hoisted up to the top of their respective scope. In your case it's always global, also for const. But variables declared as var are assigned a value of undefined whereas variables declared as let/const are not initialized at all.
You can easily reproduce this error if you uncomment either error1 or error2 in the following code. But error3 alone won't produce this ReferenceError because bar will already be initialized. You can also replace const bar = with var bar = and you will see, that you get a different error message.
process.on("uncaughtException", err => {
console.log("uncaught exception");
console.log(err);
foo();
});
function foo() {
console.log("foo");
console.log(bar.name);
}
function init() {
// throw new Error("error1");
return { name: "foobar"}
}
// throw new Error("error2");
const bar = init();
//throw new Error("error3");
I've created:
var access = fs.createWriteStream('/var/log/node/api.access.log', { flags: 'w' });
Then piped:
process.stdout.pipe(access);
Then tried:
console.log("test");
And nothing has appeared in /var/log/node/api.access.log. However this way is working:
process.stdout.pipe(access).write('test');
Could someone explain what am I doing wrong ?
I solved this problem the following way:
var access = fs.createWriteStream('/var/log/node/api.access.log');
process.stdout.write = process.stderr.write = access.write.bind(access);
Of course you can also separate stdout and stderr if you want.
I also would strongly recommend to handle uncaught exceptions:
process.on('uncaughtException', function(err) {
console.error((err && err.stack) ? err.stack : err);
});
This will cover the following situations:
process.stdout.write
process.stderr.write
console.log
console.dir
console.error
someStream.pipe(process.stdout);
throw new Error('Crash');
throw 'never do this';
throw undefined;
Checkout console.Console, the parent class of the normal console.
var myLogFileStream = fs.createWriteStream(pathToMyLogFile);
var myConsole = new console.Console(myLogFileStream, myLogFileStream);
You can then you use myConsole.log, myConsole.error, myConsole.dir, etc. and write directly to your file.
You can also monkey patch process.stdout.write as follows:
var fn = process.stdout.write;
function write() {
fn.apply(process.stdout, arguments);
myLogFileStream.write.apply(myLogFileStream, arguments);
}
process.stdout.write = write;
there are also other options for overwriting console._stdout depending on the motivation for logging the stdout to a file.
process.stdout is a Writable. pipe is a method of Readable(Cf StreamAPI documentation : https://nodejs.org/api/stream.html
You can see the documentation of process.stdout here : https://nodejs.org/api/process.html#process_process_stdout
It's surprising that you can do process.stdout.pipe(...); without any error. But i suppose this call just do nothing. Except returning a new Writable stream binded to stdout (or maybe it returns process.stdout itself. There's no specification for that in the documentation).
If you want to redirect stdout to a file, you have many solutions :
Just use your command line to do that. Windows style : node myfile.js > api.access.log.
Replace the console object by your own object. And you can rewrite console methods.
I'm not sure, but it may be possible to replace process.stdout with your own stream (and you can do whatever you want with this)
#user3173842
for the reply on
I solved this problem the following way:
var access = fs.createWriteStream('/var/log/node/api.access.log');
process.stdout.write = process.stderr.write = access.write.bind(access);
you do understand that process.stdout continues after process.on('exit') and therefore the fs.WriteStream closes after with process.stdout, according to
https://github.com/nodejs/node/issues/7606
so now the question remains, if the developer desired to have the fs.Writestream.write() return to its normal functionality and when fs.Writestream.end is called the writestream closes. How would the developer go about doing this I did
a_l = asyncify_listener
p_std_stream_m is a process stream manager object
p_std_stream_m.std_info.p_stdout_write = process.stdout.write
process.stdout.write = w_stream.write.bind(w_stream)
process.once('beforeExit', a_l( p_std_stream_m.handler,process.stdout,w_stream ) )
where in the 'beforeExit' event listener I did
process.stdout.write = p_std_stream_m.std_info.p_stdout_write
w_stream.end()
It works and you use the once method because the process.stdout seems to do a lot of work
at this time.
Is this good practice, would you do this or what would you do in this situation
anyone can feel free to reply.
Originally based on #Anatol-user3173842 answer
But in my case I needed to hook the stdout & stderr and also write into a file.
So for those who need to keep the normal stdout behaviour in addition to writing into the file. Use the following.
For non-errors:
// stdout logging hook
const stdoutWrite0 = process.stdout.write;
process.stdout.write = (args) => { // On stdout write
CustomLogger.writeToLogFile('log', args); // Write to local log file
args = Array.isArray(args) ? args : [args]; // Pass only as array to prevent internal TypeError for arguments
return stdoutWrite0.apply(process.stdout, args);
};
For errors:
// stderr logging hook
const stderrWrite0 = process.stderr.write;
process.stderr.write = (args) => { // On stderr write
CustomLogger.writeToLogFile('error', args); // Write to local error file
args = Array.isArray(args) ? args : [args]; // Pass only as array to prevent internal TypeError for arguments
return stderrWrite0.apply(process.stderr, args);
};
// uncaught exceptions
process.on('uncaughtException', (err) => {
CustomLogger.writeToLogFile('error', ((err && err.stack) ? err.stack : err));
});
Here is the CustomLogger code, where I also separate the log files by date:
export class CustomLogger {
static LOGS_DIR = 'location-of-my-log-files';
private static logDailyName(prefix: string): string {
const date = new Date().toLocaleDateString().replace(/\//g, '_');
return `${CustomLogger.LOGS_DIR}/${prefix}_${date}.log`;
}
private static writeToLogFile(prefix, originalMsg) {
const timestamp = Date.now();
const fileName = this.logDailyName(prefix);
const logMsg = prepareForLogFile(originalMsg);
fs.appendFileSync(fileName, `${timestamp}\t${logMsg}\n\n`);
return originalMsg;
}
}
Here's a quick example of a logger class that redirects stdout, stderr and exceptions to a file, while still writting everything to the console:
class Logger {
#log_stream
#stdout_write
#stderr_write
constructor(path) {
this.#log_stream = fs.createWriteStream(path, { flags: 'a' })
this.#stdout_write = process.stdout.write.bind(process.stdout)
this.#stderr_write = process.stderr.write.bind(process.stderr)
process.stdout.write = this.stdout_write.bind(this)
process.stderr.write = this.stderr_write.bind(this)
process.on('uncaughtException', function(err) {
console.error((err && err.stack) ? err.stack : err)
})
}
stdout_write(buffer) {
this.#log_stream.write(buffer)
this.#stdout_write(buffer)
}
stderr_write(buffer) {
this.#log_stream.write(buffer)
this.#stderr_write(buffer)
}
}
const logger = new Logger('example.log')
When creating new function from JavaScript code using new Function(params,body) constructor, passing invalid string in body yelds SyntaxError. While this exception contains error message (ie: Unexpected token =), but does not seem to contain context (ie. line/column or character where error was found).
Example fiddle: https://jsfiddle.net/gheh1m8p/
var testWithSyntaxError = "{\n\n\n=2;}";
try {
var f=new Function('',testWithSyntaxError);
} catch(e) {
console.log(e instanceof SyntaxError);
console.log(e.message);
console.log(e.name);
console.log(e.fileName);
console.log(e.lineNumber);
console.log(e.columnNumber);
console.log(e.stack);
}
Output:
true
(index):54 Unexpected token =
(index):55 SyntaxError
(index):56 undefined
(index):57 undefined
(index):58 undefined
(index):59 SyntaxError: Unexpected token =
at Function (native)
at window.onload (https://fiddle.jshell.net/_display/:51:8)
How can I, without using external dependencies, pinpoint SyntaxError location withinn passed string? I require solution both for browser and nodejs.
Please note: I do have a valid reason to use eval-equivalent code.
In Chromium-based browsers, as you've seen, putting try/catch around something that throws a SyntaxError while V8 is parsing the code (before actually running it) won't produce anything helpful; it will describe the line that caused the evaluation of the problematic script in the stack trace, but no details on where the problem was in said script.
But, there's a cross-browser workaround. Instead of using try/catch, you can add an error listener to window, and the first argument provided to the callback will be an ErrorEvent which has useful lineno and colno properties:
window.addEventListener('error', (errorEvent) => {
const { lineno, colno } = errorEvent;
console.log(`Error thrown at: ${lineno}:${colno}`);
// Don't pollute the console with additional info:
errorEvent.preventDefault();
});
const checkSyntax = (str) => {
// Using setTimeout because when an error is thrown without a catch,
// even if the error listener calls preventDefault(),
// the current thread will stop
setTimeout(() => {
eval(str);
});
};
checkSyntax(`console.log('foo') bar baz`);
checkSyntax(`foo bar baz`);
Look in your browser console to see this in action, not in the snippet console
Check the results in your browser console:
Error thrown at: 1:20
Error thrown at: 1:5
Which is what we want! Character 20 corresponds to
console.log('foo') bar baz
^
and character 5 corresponds to
foo bar baz
^
There are a couple issues, though: it would be good to make sure in the error listened for is an error thrown when running checkSyntax. Also, try/catch can be used for runtime errors (including syntax errors) after the script text has been parsed into an AST by the interpreter. So, you might have checkSyntax only check that the Javascript is initially parsable, and nothing else, and then use try/catch (if you want to run the code for real) to catch runtime errors. You can do this by inserting throw new Error to the top of the text that's evaled.
Here's a convenient Promise-based function which can accomplish that:
// Use an IIFE to keep from polluting the global scope
(async () => {
let stringToEval;
let checkSyntaxResolve;
const cleanup = () => {
stringToEval = null;
checkSyntaxResolve = null; // not necessary, but makes things clearer
};
window.addEventListener('error', (errorEvent) => {
if (!stringToEval) {
// The error was caused by something other than the checkSyntax function below; ignore it
return;
}
const stringToEvalToPrint = stringToEval.split('\n').slice(1).join('\n');
// Don't pollute the console with additional info:
errorEvent.preventDefault();
if (errorEvent.message === 'Uncaught Error: Parsing successful!') {
console.log(`Parsing successful for: ${stringToEvalToPrint}`);
checkSyntaxResolve();
cleanup();
return;
}
const { lineno, colno } = errorEvent;
console.log(`Syntax error thrown at: ${lineno - 1}:${colno}`);
console.log(describeError(stringToEval, lineno, colno));
// checkSyntaxResolve should *always* be defined at this point - checkSyntax's eval was just called (synchronously)
checkSyntaxResolve();
cleanup();
});
const checkSyntax = (str) => {
console.log('----------------------------------------');
return new Promise((resolve) => {
checkSyntaxResolve = resolve;
// Using setTimeout because when an error is thrown without a catch,
// even if the 'error' listener calls preventDefault(),
// the current thread will stop
setTimeout(() => {
// If we only want to check the syntax for initial parsing validity,
// but not run the code for real, throw an error at the top:
stringToEval = `throw new Error('Parsing successful!');\n${str}`;
eval(stringToEval);
});
});
};
const describeError = (stringToEval, lineno, colno) => {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
};
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
})();
Look in your browser console to see this in action, not in the snippet console
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
Result:
----------------------------------------
Syntax error thrown at: 1:29
console.log('I will throw') bar baz
^
----------------------------------------
Syntax error thrown at: 1:5
foo bar baz will throw too
^
----------------------------------------
Parsing successful for: console.log('A snippet without compile errors'); const foo = bar;
----------------------------------------
Syntax error thrown at: 2:6
With a syntax error on the second line
^
If the fact that an error is thrown at window is a problem (for example, if something else is already listening for window errors, which you don't want to disturb, and you can't attach your listener first and call stopImmediatePropagation() on the event), another option is to use a web worker instead, which has its own execution context completely separate from the original window:
// Worker:
const getErrorEvent = (() => {
const workerFn = () => {
const doEvalAndReply = (jsText) => {
self.addEventListener(
'error',
(errorEvent) => {
// Don't pollute the browser console:
errorEvent.preventDefault();
// The properties we want are actually getters on the prototype;
// they won't be retrieved when just stringifying
// so, extract them manually, and put them into a new object:
const { lineno, colno, message } = errorEvent;
const plainErrorEventObj = { lineno, colno, message };
self.postMessage(JSON.stringify(plainErrorEventObj));
},
{ once: true }
);
eval(jsText);
};
self.addEventListener('message', (e) => {
doEvalAndReply(e.data);
});
};
const blob = new Blob(
[ `(${workerFn})();`],
{ type: "text/javascript" }
);
const worker = new Worker(window.URL.createObjectURL(blob));
// Use a queue to ensure processNext only calls the worker once the worker is idle
const processingQueue = [];
let processing = false;
const processNext = () => {
processing = true;
const { resolve, jsText } = processingQueue.shift();
worker.addEventListener(
'message',
({ data }) => {
resolve(JSON.parse(data));
if (processingQueue.length) {
processNext();
} else {
processing = false;
}
},
{ once: true }
);
worker.postMessage(jsText);
};
return (jsText) => new Promise((resolve) => {
processingQueue.push({ resolve, jsText });
if (!processing) {
processNext();
}
});
})();
// Calls worker:
(async () => {
const checkSyntax = async (str) => {
console.log('----------------------------------------');
const stringToEval = `throw new Error('Parsing successful!');\n${str}`;
const { lineno, colno, message } = await getErrorEvent(stringToEval);
if (message === 'Uncaught Error: Parsing successful!') {
console.log(`Parsing successful for: ${str}`);
return;
}
console.log(`Syntax error thrown at: ${lineno - 1}:${colno}`);
console.log(describeError(stringToEval, lineno, colno));
};
const describeError = (stringToEval, lineno, colno) => {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
};
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
})();
Look in your browser console to see this in action, not in the snippet console
Essentially, what checkSyntax is doing is checking to see if the code provided can be parsed into an Abstract Syntax Tree by the current interpreter. You can also use packages like #babel/parser or acorn to attempt to parse the string, though you'll have to configure it for the syntax permitted in the current environment (which will change as new syntax gets added to the language).
const checkSyntax = (str) => {
try {
acorn.Parser.parse(str);
console.log('Parsing successful');
} catch(e){
console.error(e.message);
}
};
checkSyntax(`console.log('I will throw') bar baz`);
checkSyntax(`foo bar baz will throw too`);
checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
<script src="https://cdn.jsdelivr.net/npm/acorn#6.1.1/dist/acorn.min.js"></script>
The above works for browsers. In Node, the situation is different: listening for an uncaughtException can't be used to intercept the details of syntax errors, AFAIK. However, you can use vm module to attempt to compile the code, and if it throws a SyntaxError before running, you'll see something like this. Running
console.log('I will throw') bar baz
results in a stack of
evalmachine.<anonymous>:1
console.log('I will throw') bar baz
^^^
SyntaxError: Unexpected identifier
at createScript (vm.js:80:10)
at Object.runInNewContext (vm.js:135:10)
<etc>
So, just look at the first item in the stack to get the line number, and at the number of spaces before the ^ to get the column number. Using a similar technique as earlier, throw an error on the first line if parsing is successful:
const vm = require('vm');
const checkSyntax = (code) => {
console.log('---------------------------');
try {
vm.runInNewContext(`throw new Error();\n${code}`);
}
catch (e) {
describeError(e.stack);
}
};
const describeError = (stack) => {
const match = stack
.match(/^\D+(\d+)\n(.+\n( *)\^+)\n\n(SyntaxError.+)/);
if (!match) {
console.log('Parse successful!');
return;
}
const [, linenoPlusOne, caretString, colSpaces, message] = match;
const lineno = linenoPlusOne - 1;
const colno = colSpaces.length + 1;
console.log(`${lineno}:${colno}: ${message}\n${caretString}`);
};
checkSyntax(`console.log('I will throw') bar baz`);
checkSyntax(`foo bar baz will throw too`);
checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
Result:
---------------------------
1:29: SyntaxError: Unexpected identifier
console.log('I will throw') bar baz
^^^
---------------------------
1:5: SyntaxError: Unexpected identifier
foo bar baz will throw too
^^^
---------------------------
Parse successful!
---------------------------
2:6: SyntaxError: Unexpected identifier
With a syntax error on the second line
^
That said:
How can I, without using external dependencies, pinpoint SyntaxError location withinn passed string? I require solution both for browser and nodejs.
Unless you have to achieve this without an external library, using a library really would be the easiest (and tried-and-tested) solution. Acorn, as shown earlier (and other parsers) work in Node as well.
I'm sumarizing comments and some additional research:
Simple anwer: currently impossible
There is currently no cross-platform way to retrive syntax error position from new Function() or eval() call.
Partial solutions
Firefox support non-standard properties error.lineNumber and error.e.columnNumber. This can be used with feature detection if position of error is not critical.
There are filled bug reports/feature request for v8 that could bring support of (1) to chrome/node.js: Issue #1281, #1914, #2589
Use separate javascript parser, based on JSLint or PEG.js.
Write custom javascript parser for the job.
Solutions 1 and 2 are incomplete, rely on features that are not part of standard. They can be suitable if this information is a help, not an requirement.
Solution 3 depends on external codebase, which was explicitly required by original question. It is suitable if this information is required and larger codebase is acceptable compromise.
Solution 4 is impractical.
Credits: #user3896470, #ivan-kuckir, #aprillion
Solution for browser:
You can use latest Firefox to get the required information like error line number and column number inside the string.
Example:
var testWithSyntaxError = "{\n\n\n\nvar x=3;\n =2;}";
try {
var f=new Function('',testWithSyntaxError);
} catch(e) {
console.log(e instanceof SyntaxError);
console.log(e.message);
console.log(e.name);
console.log(e.fileName);
console.log(e.lineNumber);
console.log(e.columnNumber);
console.log(e.stack);
}
Output in Firefox console:
undefined
true
expected expression, got '='
SyntaxError
debugger eval code
6
1
#debugger eval code:4:11
Where 6 is the line number and 1 is the column number of error inside the string.
It won't work in Chrome. There are bugs filed regarding this issue for chrome browser. See:
https://bugs.chromium.org/p/v8/issues/detail?id=1281
https://bugs.chromium.org/p/v8/issues/detail?id=1914
https://bugs.chromium.org/p/v8/issues/detail?id=2589
not sure if this possible to test (might have to rethink the function).
I have this utility function:
const getProp = (propKey) => {
const env = getCurrentEnvironment();
let prop = '';
try {
prop = constants.PROPS_TABLE[env][propKey];
} catch (err) {
const errorMsg = `Property not found for environment ${env} using key ${propKey}`;
console.error(errorMsg);
}
return prop === undefined ? '' : prop;
};
and I have tests for the happy path, but I'd like prove that the error path will call console.error with expected error message.
Is there any way to accomplish this without changing the design of this function?
First of all, the fact it is doing a side effect in I/O directly to the console isn't great - but sometimes that's life.
You can:
Keep a reference to the old console.error value - var error = console.error
Set up a spy on console.error - console.error = jest.fn()
Assert it is called expect(console.error.mock.calls[0][0]).toBe('Property not found for environment ....')` with the exact params
Revert console.error to the original value in an afterEach
All together it's something like:
var error = console.error;
afterEach(() => {
console.error = error; // revert the spy
});
test('your fn', () => {
console.error = jest.fn();
setCurrentEnvironment('test'); // or whatever getCurrentEnvironment accesses
expect(getProp('non existing')).toBe('');
expect(console.error.mocks.calls[0][0]).toBe(
'Property not found for environment test using key non existing'
);
});
I'm wondering how I'd come about getting line error in eval.
eg.,
try {
eval("var hello = 5; hello hello");
} catch(err) {
console.log(err.line) // should print 2
}
Any help would be appreciated, thanks.
If you are in Node, i'd rather use the vm package as it is safer.
Here is a working solution
const vm = require('vm');
// this is the sandbox, it gives the scrip only access to these vars, which
makes it safer than a pure eval;
const sandbox = {
count: 2
};
try {
// create script to be ran
// I use backtick for new lines
const script = new vm.Script(
`count += 1;
throw new Error('test');`
);
// create the context from the sandbox
const context = new vm.createContext(sandbox);
// run the script
script.runInContext(context, {
lineOffset: 0,
displayErrors: true,
});
} catch(e) {
console.log('Line of error :', e.stack.split('evalmachine.<anonymous>:')[1].substring(0, 1))
}
Running this code will log Line of error: 3.
Here is the doc for the vm package: https://nodejs.org/api/vm.html