Internet Explorer: Global Variables, and Stack Overflows

Stack overflow at line: 0

Every once in a while, you stumble upon a particularly strange bug. A bug that seems to defy the rules of space-time. A bug that makes you pull your hair out for days. A bug that almost certainly includes Internet Explorer. This was one of those bugs for me, and since I haven’t found a lot of information out there even now that I know what the problem was, I wanted to write it up for posterity.

It starts with what looks like a relatively benign function, that we’ll call recurse:


window.recurse = function(times)
{
    if (times !== 0)
        recurse(times - 1);
}

recurse(13);

If you pass in the magic number 13 (or anything higher), this recursion is going to fail with our stack overflow pop up. No problem, you say? You’ve fought Internet Explorer before, and you know how to get down to business. Fire up the trusty IE 8 debugger and get to work! Unfortunately for you, the debugger won’t be triggered. Just a pop up for you. If you’re feeling extra ambitious, maybe you’ll try installing Visual Studio and hooking up the advanced debugger directly to IE; I did. No such luck.

I should mention that the code above wasn’t what forced me to investigate this bug. I’m not usually in the habit of writing recursive functions that don’t actually do anything. And when your code base is tens of thousands of lines long, and your coworker has pushed a
couple thousand new lines of code (which radically alter the load process), tracking down any problem without a debugger starts to get hairy. At one point in the process, I actually stepped line by line through every single new line of code, certain I’d track down the bug. I didn’t, because not only does IE not trigger the normal exception mechanism, it doesn’t even report the error until after any waiting script executions are finished. Fun.

Thankfully, my neighbor works at Microsoft, so I was able to get an inside look into the issue. That’s where our test function comes in (thanks Microsoft). Let’s get back to it:


var recurse = function(times)
{
    if (times !== 0)
        recurse(times - 1);
}

recurse(13);
recurse(10000);

Our new version works. Can you spot the difference? We’re using a “local” (but really global considering our current scope) variable here, instead of assigning the function to a property on the window object. This tells us that it has something to do with the window “host” object, but it isn’t the whole piece of the puzzle just yet.


(function(){
    var recurse = function(times)
    {
        if (times !== 0)
            recurse(times - 1);
    }

    //we won't have access outside to recurse, so add a global ref
    window.recurse = recurse;
})();

recurse(13);

This test also works, and is pretty much the last piece of the puzzle. From this we can see that simply assigning a variable through the window object isn’t the problem, the problem is actually recursing through that variable.

In the block above, the recursion is happening through the local var, not the global reference, and so it isn’t triggering the bug. To prove that this is the case, let’s try the opposite test:


(function(){
    var r = function(times)
    {
        if (times !== 0)
            recurse(times - 1);
    }

    //we won't have access outside to recurse, so add a global ref
    window.recurse = r;
})();

recurse(13);

Here, we’re doing the recursion through the global reference, and as we expected it fails. So, the lesson to learn here is that any recursion that happens through the window object is limited to a stack depth of 12. If there was a tl;dr; to this post, it would be that.

To dig just a little bit deeper into some of the oddities of what’s going on here (and how it impacted Cappuccino), you’ll want to see this unbelievably strange (but not technically incorrect) behavior:


window === window;           //true
window.window === window;    //false

function global(){
    return (function(){return this;})();
}

global() === window;         //true
global().window === window;  //false

Technically, the behavior of the window object isn’t defined by ECMAScript or any browser standard; it’s a native object provided by the browser to the runtime environment. As a result, even though IE’s behavior here is strange, and the opposite of all other major browsers, it isn’t technically wrong. It’s absolutely terrible, though.

Why did we have this problem in the first place? To answer that, you need to know a bit about CommonJS modules. In the CommonJS “spec” modules have their own scope, and don’t export their vars as globals to the outside world. Instead, the module loader passes in an “exports” object, and the module author assigns properties to that object. Another way to put it would be to say that the public API of any module has to be explicitly created by assigning methods to the exports object.

In addition to Cappuccino, we’re also the authors of Narwhal, the most popular implementation of the CommonJS spec. We’ve already migrated most of our tools to run on top of Narwhal, and we’re continuing to improve the integration between Objective-J and Narwhal. Part of that means changing some of our code to use the exports technique, and using that same code in the browser.

What we ended up with was something like this:


var exports = {};
(function(global, exports){

    //lots of methods...

    function objj_msgSend(){
        //do some stuff;
    }

    //lots of exports...
    exports.objj_msgSend = objj_msgSend;

    // make exports global
    for (var export in exports)
        if (exports.hasOwnProperty(export))
            global[export] = exports[export];

})(window, exports)

The final loop assigns the exports to the global scope, since that’s how Objective-J code is expected to run. This last step was the cause of all of our problems, because as you can see, we’re creating all global methods on the window object, which means all calls to them are going to go through the host object, and be subject to our exceptionally low recursion limit.

Our first fix looked like this:


    for (var export in exports)
        if (exports.hasOwnProperty(export))
            eval(export +" = global[""+export+""];");

The eval creates a global implicitly, rather than explicitly, which doesn’t trigger the trip through the host object. We explored a few different ways to get around the bug, including working with Microsoft, but found no other viable solution.

Rather than ship this code in Cappuccino 0.8, though, we decided to take a step back and re-examine our initial requirements. The desire to strictly adhere to the CommonJS proposal was requiring that our code become more complex and harder to maintain, so we dropped that requirement.

The final version of the code is almost exactly the same, except rather than doing things like global.foo = function(){}, we’re simply using implicit global creation directly. This is more in line with the way all existing JavaScript runtimes actually work. It’s also the way Narwhal works, and probably the way CommonJS should work.

I want to give another thank you to all the folks at Microsoft who helped us track down the problem and provided us with a simple test case that reproduced the problem. They were quite helpful, and we (along with all of our users) appreciate it. Now hopefully we’ll see this issue addressed in IE9, along with a faster JavaScript engine :) .

Update: Re-reading this post a year later, I realize I never made it clear that the issue isn’t just recursion. IE won’t support any execution stack longer than 12 frames. — Ross

  • steveplatz

    Maybe I'm missing something, but how is

    (function(){
    var recurse = function(times)
    {
    if (times !== 0)
    recurse(times – 1);
    }

    //we won't have access outside to recurse, so we add a global reference
    window.recurse = recurse;
    })();

    recurse(13);

    Any different than:

    (function(){
    var r = function(times)
    {
    if (times !== 0)
    recurse(times – 1);
    }

    //we won't have access outside to recurse, so we add a global reference
    window.recurse = r;
    })();

    recurse(13);

    To me, it just appears that

    var recurse

    was changed to

    var r

    Same value, different name. Am I missing something?

  • boucher

    If you look at the code inside the function, you see we're recursing using the word recurse in both scenarios.

    In the first scenario, recurse is a local variable. So, we recurse through a local variable.

    In the second scenario, r is the local, and recurse is a global variable. Because there's no local recurse, just r, it goes up the global scope, to the recurse we defined by writing window.recurse = r;

    That second scenario is the problem, because the recursion is now happening through the host object, the source of all our problems.

  • boucher

    If you look at the code inside the function, you see we're recursing using the word recurse in both scenarios.

    In the first scenario, recurse is a local variable. So, we recurse through a local variable.

    In the second scenario, r is the local, and recurse is a global variable. Because there's no local recurse, just r, it goes up the global scope, to the recurse we defined by writing window.recurse = r;

    That second scenario is the problem, because the recursion is now happening through the host object, the source of all our problems.

  • steveplatz

    I see it now. Thank you! I didn't catch that and I must have compared the two examples 10 times before commenting.

  • joely

    This fascinating article reads like a technical thriller. Your tenacity and commitment to quality set the highest standard.

  • joely

    This fascinating article reads like a technical thriller. Your tenacity and commitment to quality set the highest standard.

  • http://www.zachleat.com/ Zach Leatherman

    This sounds very similar to some research I went through when I found out that you can't reuse argument variables in Internet Explorer.

  • krcko

    This saved me! Thank you!
    I was debugging the exact same error in past few days! with no luck of spotting the problem :|

  • jiaaro

    I've run headlong into this bug before as well :( and I also do not use recursion very often. Never did figure it out though, so thanks!

  • deadlyicon

    shouldn't
    if (exports[export].hasOwnProperty(export))
    be
    if (exports.hasOwnProperty(export))
    ?

  • boucher

    Yeah, that's more accurate. It was kind of pseudo code already. I've made the change, thanks.

  • deadlyicon

    awesome. Glad I didn't miss read you well written article. FYI this is one
    more

    http://idisk.me.com/deadlyicon/Public/Pictures/…

    Nice Find!, You Rock!

    Jared

  • kangax

    [removed]

  • kangax

    It's sad that resolution of this problem involves undeclared assignments — something that's been considered a bad practice for years.

    FWIW, ES5-strict throws ReferenceError in cases like that (to be more precise, when LHS operand evaluates to unresolvable reference). This means that “exports” snippet you presented is not “ES5-strict”-compliant, naturally.

    Another problem is MSHTML DOM, and the way it throws error when identifier on LHS of undeclared assignment is same as _name_ or _id_ of one of the elements in a document. You can see an example of it in my blog (http://perfectionkills.com/onloadfunction-consi…).

    This last issue is probably more important (and likely to occur) than any other one.

  • jdalton

    shouldn't `eval(export +” = global[""+export+""];”);` be `eval(export +” = exports['"+export+"'];”);`

  • Joe

    I stumbled across a different problem with IE6 but with the same cause, which I have written up at http://zebracorner.blogspot.com/2010/03/yesterd…

  • Joe

    I discovered another IE6 defect with the same cause yesterday – see
    http://zebracorner.blogspot.com/2010/03/yesterd…

  • http://website-in-a-weekend.net/ Dave Doolin

    Shoot this over to Crockford at jslint. It's important!

  • http://hubpages.com/hub/800-Calorie-Diet 800 calorie diet

    Golden. Great, useful info.

  • http://www.discount-air-jordan.com nike air jordan

    Mark S. is definitely on the right track. If you want to get a professional looking email address, Id recommend buying your name domain name, like or
    air jordan 17.5
    If its common it might be difficult to get, however, be creative and you can usually find something.

  • Daniel

    Great article, I would have been lost without it.

    Just wanted to point out: this behavior not only occurs with functions assigned to window, but also with methods added to DOM elements. Im my case, I was using the Prototype function Element.addMethods().

  • Aseire

    Gura Mile Maith Agat (A Million thanks to you) Ross Boucher! Your solution solved my IE JavaScript “Stack overflow in line 0″ errors perfectly and saved me an enormous amount of wasted time, wasted effort and incredible frustration. To compound matters, in my case, the user set variable rates of recursion and the vast majority of user cases were under the 13 recursion limit and so masked the problem! In addition the entire application (including recursion) worked perfectly in FireFox. You represent Engineering at its best!

blog comments powered by Disqus

Download

Cappuccino and Objective-J are licensed under the LGPL. For more information, see our licensing page.

Copyright © 2008-2011 - 280 North, Inc. Cappuccino and Objective-J are registered Trademarks of 280 North. Logo by Sofa. Hosting by Slicehost.