Inconsistent scope of "use strict" on different web browsers (concerning arguments.callee and caller)

Pang picture Pang · Jun 1, 2013 · Viewed 22.5k times · Source

Situation:

I found something strange concerning strict mode in Javascript.

  • I am using an external, third-party Javascript library which
    • was minified,
    • has over 4000 lines of code,
    • is not using use strict at all, and
    • is using arguments.callee.
  • I am using use strict in my own code, scoped within a function.

When I call one of the functions provided by the library, it throws an error. However,

  • the error is thrown only if I am using use strict
  • the error is thrown in all browsers except Chrome

Code:

I've removed all the unrelated stuff and reduced the code into this (online demo on jsFiddle):


Test result:

+-----------------------+-----+--------------------------------------------------------------+
| Browser               | OS  | Error                                                        |
+-----------------------+-----+--------------------------------------------------------------+
| Chrome 27.0.1453.94 m | Win | <<NO ERROR!>>                                                |
| Opera 12.15           | Win | Unhandled Error: Illegal property access                     |
| Firefox 21.0          | Win | TypeError: access to strict mode caller function is censored |
| Safari 5.1.7          | Win | TypeError: Type error                                        |
| IE 10                 | Win | SCRIPT5043: Accessing the 'caller' property of a function or |
|                       |     |             arguments object is not allowed in strict mode   |
| Chrome 27.0.1543.93   | Mac | <<NO ERROR!>>                                                |
| Opera 12.15           | Mac | Unhandled Error: Illegal property access                     |
| Firefox 21.0          | Mac | TypeError: access to strict mode caller function is censored |
| Safari 6.0.4          | Mac | TypeError: Function.caller used to retrieve strict caller    |
+-----------------------+-----+--------------------------------------------------------------+

Note: for OS, Win = Windows 7, Mac = Mac OS 10.7.5


My understanding:

  • All modern desktop browsers support use strict (see Can I use).
  • The use strict is scoped within my function, so everything defined outside its scope is not affected (see this Stack Overflow question).

Question:

So, are all browsers except Chrome wrong? Or is it the other way round? Or is this undefined behaviour so the browsers may choose to implement it in either way?

Answer

T.J. Crowder picture T.J. Crowder · Jun 1, 2013

Preface

A couple of quick points before we get into the meat of this:

  • All modern desktop browsers support use strict...

No, not at all. IE8 is a fairly modern browser (not anymore, in 2015), and IE9 is a quite fairly modern browser. Neither of them supports strict mode (IE9 supports parts of it). IE8 is going to be with us a long time, because it's as high as you can go on Windows XP. Even though XP is now flatly end-of-lifed (well, you can buy a special "Custom Support" plan from MS), people will continue to use it for a while.

  • The use strict is scoped within my function, so everything defined outside its scope is not affected

Not quite. The specification imposes restrictions on how even non-strict code uses functions created in strict mode. So strict mode can reach outside its box. And in fact, that's part of what's going on with the code you're using.

Overview

So, are all browsers except Chrome wrong? Or is it the other way round? Or is this undefined behaviour so the browsers may choose to implement it in either way?

Looking into it a bit, it looks like:

  1. Chrome is getting it right one way,

  2. Firefox is getting it right a different way,

  3. ...and IE10 is getting it very slightly wrong. :-) (IE9 definitely gets it wrong, although not in a particularly harmful way.)

I didn't look at the others, I figured we'd covered the ground.

The code fundamentally causing the trouble is this loop

var a5 = arguments.callee;
while (a5) {
    a5 = a5.caller      // Error on this line in all browsers except Chrome
}

...which relies on the caller property of function objects. So let's start there.

Function#caller

The Function#caller property was never defined in the 3rd edition specification. Some implementations provided it, others didn't. It's a shockingly bad idea (sorry, that was subjective, wasn't it?) an implementation issue (even more of one than arguments.caller), particularly in multi-threaded environments (and there are multi-threaded JavaScript engines), as well as with recursive code, as Bergi pointed out in the comments on the question.

So in the 5th edition they explicitly got rid of it, by specifying that referencing the caller property on a strict function would throw an error. (This is in §13.2, Creating Function Objects, Step 19.)

That's on a strict function. On a non-strict function, though, the behavior is unspecified and implementation-dependent. Which is why there are so many different ways to get this right.

Instrumented Code

It's easier to refer back to instrumented code than a debugging session, so let's use this:

console.log("1. Getting a5 from arguments.callee");
var a5 = arguments.callee;
console.log("2. What did we get? " +
            Object.prototype.toString.call(a5));
while (a5) {
    console.log("3. Getting a5.caller");
    a5 = a5.caller;      // Error on this line in all browsers except Chrome
    console.log("4. What is a5 now? " +
                Object.prototype.toString.call(a5));
}

How Chrome Gets It Right

On V8 (Chrome's JavaScript engine), the code above gives us this:

1. Getting a5 from arguments.callee
2. What did we get? [object Function]
3. Getting a5.caller
4. What is a5 now? [object Null]

So we got a reference to the foo.bar function from arguments.callee, but then accessing caller on that non-strict function gave us null. The loop terminates and we don't get any error.

Since Function#caller is unspecified for non-strict functions, V8 is allowed to do anything it wants for that access to caller on foo.bar. Returning null is perfectly reasonable (although I was surprised to see null rather than undefined). (We'll come back to that null in the conclusions below...)

How Firefox Gets It Right

SpiderMonkey (Firefox's JavaScript engine) does this:

1. Getting a5 from arguments.callee
2. What did we get? [object Function]
3. Getting a5.caller
TypeError: access to strict mode caller function is censored

We start out getting foo.bar from arguments.callee, but then accessing caller on that non-strict function fails with an error.

Since, again, the access to caller on a non-strict function is unspecified behavior, the SpiderMonkey folks can do what they want. They decided to throw an error if the function that would be returned is a strict function. A fine line, but as this is unspecified, they're allowed to walk it.

How IE10 Gets It Very Slightly Wrong

JScript (IE10's JavaScript engine) does this:

 1. Getting a5 from arguments.callee 
 2. What did we get? [object Function] 
 3. Getting a5.caller 
SCRIPT5043: Accessing the 'caller' property of a function or arguments object is not allowed in strict mode

As with the others, we get the foo.bar function from arguments.callee. Then trying to access that non-strict function's caller gives us an error saying we can't do that in strict mode.

I call this "wrong" (but with a very lower-case "w") because it says that we can't do what we're doing in strict mode, but we're not in strict mode.

But you could argue this is no more wrong that what Chrome and Firefox do, because (again) the caller access is unspecified behavior. So the IE10 folks decided that their implementation of this unspecified behavior would throw a strict-mode error. I think it's misleading, but again, if it's "wrong," it certainly isn't very wrong.

BTW, IE9 definitely gets this wrong:

1. Getting a5 from arguments.callee 
2. What did we get? [object Function] 
3. Getting a5.caller 
4. What is a5 now? [object Function] 
3. Getting a5.caller 
4. What is a5 now? [object Null]

It allows Function#caller on the non-strict function, and then allows it on a strict function, returning null. The spec is clear that that second access should have thrown an error, as it was accessing caller on a strict function.

Conclusions and Observations

What's interesting about all of the above is that in addition to the clearly-specified behavior of throwing an error if you try to access caller on strict functions, Chrome, Firefox, and IE10 all (in various ways) prevent your using caller to get a reference to a strict function, even when accessing caller on a non-strict function. Firefox does this by throwing an error. Chrome and IE10 do it by returning null. They all support getting a reference to a non-strict function via caller (on a non-strict function), just not a strict function.

I can't find that behavior specified anywhere (but then, caller on non-strict functions is entirely unspecified...). It's probably the Right Thing(tm), I just don't see it specified.

This code is also fun to play with: Live Copy | Live Source

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Strict and Loose Function#caller</title>
  <style>
    p {
      font-family: sans-serif;
      margin: 0.1em;
    }
    .err {
      color: #d00;
    }
  </style>
</head>
<body>
  <script>
    function display(msg, cls) {
        var p = document.createElement('p');
        if (cls) {
            p.className = cls;
        }
        p.innerHTML = String(msg);
        document.body.appendChild(p);
    }

    // The loose functions
    (function () {
      function loose1() {
        display("loose1 calling loose2");
        loose2();
      }
      loose1.id = "loose1"; // Since name isn't standard yet

      function loose2() {
        var c;

        try {
          display("loose2: looping through callers:");
          c = loose2;
          while (c) {
            display("loose2: getting " + c.id + ".caller");
            c = c.caller;
            display("loose2: got " +
                    ((c && c.id) || Object.prototype.toString.call(c)));
          }
          display("loose2: done");
        }
        catch (e) {
          display("loose2: exception: " +
                  (e.message || String(e)),
                  "err");
        }
      }
      loose2.id = "loose2";

      window.loose1 = loose1;

      window.loose2 = loose2;
    })();

    // The strict ones
    (function() {
      "use strict";

      function strict1() {
        display("strict1: calling strict2");
        strict2();
      }
      strict1.id = "strict1";

      function strict2() {
        display("strict2: calling loose1");
        loose1();
      }
      strict2.id = "strict2";

      function strict3() {
        display("strict3: calling strict4");
        strict4();
      }
      strict3.id = "strict3";

      function strict4() {
        var c;

        try {
          display("strict4: getting strict4.caller");
          c = strict4.caller;
        }
        catch (e) {
          display("strict4: exception: " +
                  (e.message || String(e)),
                 "err");
        }
      }
      strict4.id = "strict4";

      strict1();      
      strict3();
    })();
  </script>
</body>
</html>