Get caller from Error.protoype.stack in Chrome/Firefox/Safari browsers on Windows/macOS

Typically in JavaScript and Node.js projects, use of console is disallowed and is enforced using the no-console rule in ESLint. Logging would then be done thru a centralized function, e.g. log(), which may use console.log with the ESLint rule disabled just for that line of code, instead of having multiple console.log statements throughout the codebase and disabling the ESLint rule in multiple locations.

A drawback to this is that information on the caller is lost, e.g. the function/filename/line/character accompanying the log in the browser DevTools console would belong to the declaration of log() and not the actual function that called it. A workaround is to instantiate an Error object inside log() and retrieve the caller information from the stack trace in Error.prototype.stack.

Unfortunately, the stack trace is a string and not an array, and the output varies across different browsers. Before we continue, here’s the sample code which we will refer to for the stack trace.

<script> // do not copy this tag when pasting in browser DevTools
    var test = (function () {
        // Get caller of logInfo() from Error.protoype.stack in Chrome/Firefox/Safari on Windows/macOS
        async function second() {
            return new Promise((resolve, reject) => {
                logInfo('demo'); // caller (line 6 in webpage source code, line 5 if pasted in browser DevTools)
            });
        }

        function first() {
            second();
        }

        function logInfo(...messages) {
            log('info', messages);
        }

        function log(severity, messages) {
            let error = new Error();
            console.log(JSON.stringify({ error_stack: error.stack }));
        }

        // Initialization
        (function init() {
            first();
        })();
    })();
</script> <!-- do not copy this tag when pasting in browser DevTools -->

And here’s the output of (new Error()).stack across Chrome/Firefox/Safari, from opening a webpage and pasting the code directly in the browser DevTools.

// Chrome on Windows/macOS (webpage)
Error
    at log (https://example.com/error.html:19:25)
    at logInfo (https://example.com/error.html:15:13)
    at https://example.com/error.html:6:17
    at new Promise (<anonymous>)
    at second (https://example.com/error.html:5:20)
    at first (https://example.com/error.html:11:13)
    at init (https://example.com/error.html:29:13)
    at https://example.com/error.html:30:11
    at https://example.com/error.html:31:7

// Chrome on Windows/macOS (direct paste)
Error
    at log (<anonymous>:18:25)
    at logInfo (<anonymous>:14:13)
    at <anonymous>:5:17
    at new Promise (<anonymous>)
    at second (<anonymous>:4:20)
    at first (<anonymous>:10:13)
    at init (<anonymous>:28:13)
    at <anonymous>:29:11
    at <anonymous>:30:7

// Firefox on Windows/macOS (webpage)
log@https://example.com/error.html:19:25
logInfo@https://example.com/error.html:15:16
second/test<@https://example.com/error.html:6:24
second@https://example.com/error.html:5:20
first@https://example.com/error.html:11:13
init@https://example.com/error.html:29:13
test<@https://example.com/error.html:30:11
@https://example.com/error.html:31:7

// Firefox on Windows/macOS (direct paste)
log@debugger eval code:18:25
logInfo@debugger eval code:14:16
second/test<@debugger eval code:5:24
second@debugger eval code:4:20
first@debugger eval code:10:13
init@debugger eval code:28:13
test<@debugger eval code:29:11
@debugger eval code:30:7

// Safari on macOS (webpage)
log@https://example.com/error.html:19:34
logInfo@https://example.com/error.html:15:16
@https://example.com/error.html:6:24
Promise@[native code]
@https://example.com/error.html:5:31
second@https://example.com/error.html:4:39
first@https://example.com/error.html:11:19
init@https://example.com/error.html:29:18
@https://example.com/error.html:30:11
global code@https://example.com/error.html:31:7

// Safari on macOS (direct paste)
log@
logInfo@
@
Promise@[native code]
@
second@
first@
init@
@
global code@

As can be seen from the strings (not arrays) above, the delimiters differ, with Chrome using ” at ” and Firefox/Safari using “@”, making it hard to split the string into an array consistently across all the 3 browsers. One way would be to replace the different delimiters with a common delimiter before splitting the string using the common delimiter. In the solution below, the getCaller() function does this and returns the 3rd line in the stack trace, the actual location which called logInfo() in our sample code, consistently across Chrome/Firefox/Safari browsers.

<script>
    var test = (function () {
        // Get caller of logInfo() from Error.protoype.stack in Chrome/Firefox/Safari on Windows/macOS
        async function second() {
            return new Promise((resolve, reject) => {
                logInfo('demo'); // caller (line 6 in webpage source code), stacktrace level 3
            });
        }

        function first() {
            second();
        }

        function logInfo(...messages) {
            log('info', messages); // stacktrace level 2
        }

        function log(severity, messages) {
            let error = new Error(); // stacktrace level 1
            console.log(JSON.stringify(
                {
                    severity: severity,
                    messages: messages,
                    caller: getCaller(error),
                    errorStack: error.stack,
                },
                null,
                2
            ));
        }

        /**
         * @param {Error} error
         * @returns {string}
         */
        function getCaller(error) {
            let stack = (error?.stack || error || '').toString().trim();
            let resolvedStack = stack
                .replace(/((Error\s+)? at )/gi, '***') // Chrome (Error title joined with 1st line)
                .replace(/([^@\n]*@)/gi, '***$1'); // Firefox/Safari
            let stackArr = resolvedStack.split('***');

            return (stackArr?.[3] || '').trim();
        }

        /**
         * Initialization
         */
        (function init() {
            first();
        })();
    })();
</script>