Traversing arguments.callee.caller causes an infinite loop

Say I want to get some kind of stack trace, getting the name of all the functions that have been called before the current function.

I do something like this:

        var callee;
        var caller;
        var _args = arguments;
        var check = 0;
        do {
                    check++;

                    callee = _args.callee;
                    caller = callee.caller;

                    var msg = 'Check ' + check + ' - ' + callee.name 
                            + ' has been called by: ' + caller.name;
                    console.log(msg);

                    if (caller) {
                        // Get this caller's arguments
                        _args = caller.arguments;
                    } else {
                        reached_end = true;
                    }

        } while (!reached_end);

This works fine, most of the time. But sometimes it gets stuck in an infinite loop, and I wonder: how is that possible? And what can I do about it?

Here's the output of my infinite-loop:

    Check 1 - __parent__ has been called by: add
    Check 2 - add has been called by: afterComponentStartup
    Check 3 - afterComponentStartup has been called by: _launchComponents [arg0:"startup"]
    Check 4 - _launchComponents has been called by: beforeActionNext
    Check 5 - beforeActionNext has been called by: beforeAction
    Check 6 - beforeAction has been called by: afterComponentInitialize
    Check 7 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
    Check 8 - _launchComponents has been called by: beforeActionNext
    Check 9 - beforeActionNext has been called by: beforeAction
    Check 10 - beforeAction has been called by: afterComponentInitialize
    Check 11 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
    Check 12 - _launchComponents has been called by: beforeActionNext
    Check 13 - beforeActionNext has been called by: beforeAction
    Check 14 - beforeAction has been called by: afterComponentInitialize
    Check 15 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
    Check 16 - _launchComponents has been called by: beforeActionNext
    Check 17 - beforeActionNext has been called by: beforeAction
    Check 18 - beforeAction has been called by: afterComponentInitialize
    Check 19 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
    Check 20 - _launchComponents has been called by: beforeActionNext
    Check 21 - beforeActionNext has been called by: beforeAction
    Check 22 - beforeAction has been called by: afterComponentInitialize
    Check 23 - afterComponentInitialize has been called by: _launchComponents [arg0:"startup"]
    Check 24 - _launchComponents has been called by: beforeActionNext
    Check 25 - beforeActionNext has been called by: beforeAction
    Check 26 - beforeAction has been called by: afterComponentInitialize

arguments.callee.caller points to the function reference, of which it only exists one of per function in each call stack.

Every time a function gets invoked the caller property gets set, which means that if the same function has been called multiple times in a call stack, as would happen with recursive functions, the previous value would get reset, and caller would now point to itself. This is what is causing your infinite loops.

So in your algorithm, if you get to a point where callee === callee.caller you need to break in order for this not to happen.

You need to add the callers in an array and check whether the next caller is in the array. If so then you have a cyclic or recursive call structure. Try this:

var args = arguments;
var callee = args.callee;
var caller = callee.caller;

var stack = [callee];

while (caller) {
    if (stack.indexOf(caller) < 0) {
        stack.push(caller);
        args = caller.arguments;
        callee = args.callee;
        caller = callee.caller;
    } else break;
}

console.log(stack);

I've found a way to pass state information (var .ua (Object) in this case) into the callstack of recursive functions;

instead of

var fwa.animatedJavascriptControlCenter = {
    ...
    ,
    scanElements : function (el, scanResult) { 
    // a function that walks the DOM tree to find elements that require molding by my js framework
    if (!scanResult) scanResult={};
    if (el.tagName.toLowerCase()!='svg') {
        if (conditions_for_this_element_met(el)) {
            scanResults[el.id] = el;
        };
        if (el.children.length>0) {
            for (var i=0; i < el.children.length; i++) {
                if (el.children[i].tagName.toUpperCase()!=='IFRAME') {
                    scanResult = fwa.animatedJavascriptControlCenter.scanElements(el.children[i], scanResult);
                }
            }
        }
    }
    return scanResult;
    },
    ...
}

do:

var fwa.animatedJavascriptControlCenter = {
    ...
    ,
    scanElements : function (el, scanResult) { 
        if (!scanResult) scanResult={};
        if (el.tagName.toLowerCase()!='svg') {
            if (conditions_for_this_element_met(el)) {
                scanResults[el.id] = el;
            }
            if (el.children.length>0) {
                for (var i=0; i < el.children.length; i++) {
                    if (el.children[i].tagName.toUpperCase()!=='IFRAME') {
                        var args = [el.children[i], scanResult];
                        args.ua= tracer.findUA(arguments);
                        var passUAfunc = function(scanResult) {
                            return fwa.animatedJavascriptControlCenter.scanElements(el.children[i], scanResult);
                        }
                        passUAfunc.ua = args.ua;
                        scanResult = passUAfunc(scanResult);
                    }
                }
            }
        }
        return scanResult;
    },
    ...
}

to make this work, you'll also need:

// Bonus : ;-)
// Enable the passage of the 'this' object through the JavaScript timers
// thanks to https://developer.mozilla.org/en/docs/DOM/window.setTimeout#Callback_arguments
var __nativeST__ = window.setTimeout, __nativeSI__ = window.setInterval;
window.setTimeout = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
    var 
    oThis = this, 
    ua = tracer.findUA(arguments),
    aArgs = Array.prototype.slice.call(arguments, 2);
    if (ua) aArgs.ua = ua;
    //if (!ua) debugger;
    return __nativeST__(vCallback instanceof Function ? function () {
        vCallback.apply(oThis, aArgs);
    } : vCallback, nDelay);
};
window.setInterval = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
    var 
    oThis = this, 
    aArgs = Array.prototype.slice.call(arguments, 2),
    ua = tracer.findUA(arguments);
    if (ua) aArgs.ua = ua;
    return __nativeSI__(vCallback instanceof Function ? function () {
        vCallback.apply(oThis, aArgs);
    } : vCallback, nDelay);
};

var tracer = {
/*  object last modified : 14 May 2013, 04:46 CEST

    original code by http://javascriptweblog.wordpress.com/2010/06/01/a-tracer-utility-in-2kb/

    modified by rene7705@gmail.com with:
    (1) http://stackoverflow.com/a/15582432/2379929

    augmented by rene7705@gmail with (among other things):
    http://stacktracejs.com/

    this code is now used in and offered as part of the web-framework at http://fancywebapps.com
    (component name : hipLog, to be released later)
*/  


    nativeCodeEx: /\[native code\]/,
    tracing: [],
    traced : [],
    userActions : [],

    findUA : function (arg) {
        var p = arg;
        if (p.ua) return p.ua;

        var callee = arg.callee;
        var caller = callee.caller;

        var stack = [callee];

        while (caller) {
            if (stack.indexOf(caller) < 0) {
                stack.push(caller);
                args = caller.arguments;
                callee = args.callee;
                caller = callee.caller;
            } else break;
        }


        while (p = stack.shift()) {
            if (p.ua) return p.ua;
            if (p.arguments && p.arguments.ua) return p.arguments.ua;

        };
        return false;
    },

    traceMe: function(func, methodName, path) {
        var traceOn = function() {
            var 
            ua = tracer.findUA(arguments),
            startTime = +new Date;

            if (!ua){
                //debugger;
                //ua = tracer.findUA(arguments);
            };
            //if (path=='fwa.animatedJavascriptControlCenter.scanElements') debugger;

            if (!ua) {
                //debugger;
                var
                uaIdx = tracer.userActions.length,
                ua = tracer.userActions[uaIdx] = {
                    uaIdx : uaIdx,
                    startTime : startTime,
                    path : path,
                    stackLevel : 0
                };
                tracer.traced[uaIdx] = [];
            } else {
                var uaIdx = ua.uaIdx;
                ua.stackLevel++;
            }

            arguments.ua = ua;

            var idx = tracer.traced[uaIdx].length;
            tracer.traced[uaIdx][idx] = {
                path : path,
                arguments : arguments
            }; 

            var result = func.apply(this, arguments);

            tracer.traced[uaIdx][idx].stacktrace = printStackTrace() // see http://stacktracejs.com, free, quite useful
            tracer.traced[uaIdx][idx].result = result;
            tracer.traced[uaIdx][idx].timed = new Date - startTime;  
            tracer.traced[uaIdx][idx].stackLevel = ua.stackLevel;
            ua.stackLevel--;
            return result;
        };
        traceOn.traceOff = func;
        for (var prop in func) {
            traceOn[prop] = func[prop];
        }
        console.log("tracing " + path);
        return traceOn;
    },

    traceAll: function(root, path, recurse) {
        if ((root == window) || !((typeof root == 'object') || (typeof root == 'function'))) {return;}
        for (var key in root) {
            if ((root.hasOwnProperty(key)) && (root[key] != root)) {
                var thisObj = root[key];
                if (typeof thisObj == 'function') {
                    if ((this != root) && !thisObj.traceOff && !this.nativeCodeEx.test(thisObj)) {
                        root[key] = this.traceMe(root[key], key, path+'.'+key);
                        this.tracing.push({obj:root,methodName:key, path:path+'.'+key});
                    }
                }
                recurse && this.traceAll(thisObj, path+'.'+key, true);
             }
        }
    },

    untraceAll: function() {
        for (var i=0; i<this.tracing.length; ++i) {
            var thisTracing = this.tracing[i];
            thisTracing.obj[thisTracing.methodName] =
                thisTracing.obj[thisTracing.methodName].traceOff;
        }
        //console.log("tracing disabled");
        tracer.tracing = [];
    }
}

to be used like:

// call this just after _completely_ defining yourFrameworkRootObject and yourSiteCodeRootObject
tracer.traceAll (yourFrameworkRootObject, true);
tracer.traceAll (yourSiteCodeRootObject, true);

FYI: this code was taken from my js framework http://fancywebapps.com, and will be part of it's hipLog component that is designed to show you the complete execution path (with or without round trips to the server and/or setTimeout()s) of any userAction like clicks, hovers, or page loads.