08.09.10

New ES5 strict mode support: now with poison pills!

tl;dr

Don’t try to use the arguments or caller properties of functions created in strict mode code. Don’t try to use the callee or caller properties of arguments objects corresponding to invocations of functions in strict mode code. Don’t try to use the caller property of a function if it might be called from strict mode code. You are in for an unpleasant surprise (a thrown TypeError) if you do.

function strict() { "use strict"; return arguments; }
function outer() { "use strict"; return inner(); }
function inner() { return inner.caller; }

strict.caller;    // !!! BAD IDEA
strict.arguments; // !!! BAD IDEA
strict().caller;  // !!! BAD IDEA
strict().callee;  // !!! BAD IDEA
outer();          // !!! BAD IDEA

Really, it’s best not to access the caller of a function, the current function (except by naming it), or the arguments for a given function (except via arguments or by use of the named parameter) at all.

ES5 strict mode: self-limitation, not wish fulfillment

ES5 introduces the curious concept of strict mode. Strict mode, whose name and concept derive from the similar feature in Perl, is a new feature in ES5 whose purpose is to deliberately reduce the things you can do in JavaScript. Instead of a feature, it’s really more the absence of several features, within the scope of the strict-annotated code: with, particularly intrusive forms of eval, silent failure of writes to non-writable properties, silent failure of deletions of non-configurable properties, implicit creation of global-object properties, and so on. The goal of these removals is to simplify both the reasoning necessary to understand such code and the implementation of it in JavaScript engines: to sand away some rough edges in the language.

Magical stack-introspective properties of functions and arguments

Consider this code, and note the expected behavior (expected per the web, but not as part of ES3):

function getSelf() { return arguments.callee; }
assertEq(getSelf(), getSelf); // arguments.callee is the enclosing function

function outer()
{
  function inner() { return arguments.callee.caller; } // inner.caller === outer
  return inner();
}
assertEq(outer(), outer); // fun.caller === nearest function in stack that called fun, or null

function args2()
{
  return args2.arguments;
}
assertEq(args2(17)[0], 17); // fun.arguments === arguments object for nearest call to fun in stack, or null

Real-world JavaScript implementations take many shortcuts for top performance. These shortcuts are not (supposed to be) observable, except by timing the relevant functionality. Two common optimizations are function inlining and avoiding creating an arguments object. The above “features” play havoc with both of these optimizations (as well as others, one of which will be the subject of a forthcoming post).

Inlining a function should conceptually be equivalent to splatting the function’s contents in that location in the calling function and doing some α-renaming to ensure no names in the splatted body conflict with the surrounding code. The ability to access the calling function defeats this: there’s no function being invoked any more, so what does it even mean to ask for the function’s caller? (Don’t simply say you’d hard-code the surrounding function: how do you know which property lookups in the inlined code will occur upon precisely the function being called, looking for precisely the caller property?) It is also possible to access a function’s arguments through fun.arguments. While the “proper” behavior here is more obvious, implementing it would be a large hassle: either the arguments would have to be created when the function was inlined (in the general case where you can’t be sure the function will never be used this way), or you’d have to inline code in such a way as to be able to “work backward” to the argument values.

Speaking of arguments, in offering access to the corresponding function via arguments.callee it has the same problems as fun.caller. It also presents one further problem: in (some, mostly old) engines, arguments.caller provides access to the variables declared within that function when it was most recently called. (If you’re thinking security/integrity/optimization hazard, well, you now know why engines no longer support it.)

In sum these features are bad for optimization. Further, since they’re a form of dynamic scoping, they’re basically bad style in many other languages already.

Per ES5, SpiderMonkey no longer supports this stack-inspecting magic when it interacts with strict mode

As of the most recent Firefox nightly, SpiderMonkey now rejects code like that given above when it occurs in strict mode (more or less). (The properties in question are now generally implemented through a so-called “poison pill” mechanism, an immutable accessor property which throws a TypeError when retrieved or set.) The specific scenarios which we reject are as follows.

First, attempts to access the caller or arguments (except by directly naming the object) of a strict mode function throw a TypeError, because these properties are poison pills:

function strict()
{
  "use strict";
  strict.caller;    // !!! TypeError
  strict.arguments; // !!! TypeError
  return arguments; // direct name: perfectly cromulent
}
strict();

Second, attempts to access the enclosing function or caller variables via the arguments of a strict mode function throw a TypeError. These properties too are poison pills:

function strict()
{
  "use strict";
  arguments.callee; // !!! TypeError
  arguments.caller; // !!! TypeError
}
strict();

Third (and most trickily, because non-strict code is affected), attempts to access a function’s caller when that caller is in strict mode will throw a TypeError. This isn’t a poison pill, because if the "use strict" directive weren’t there it would still “work”:

function outer()
{
  "use strict";
  return inner();
}
function inner()
{
  return inner.caller; // !!! TypeError
}
outer();

But if there’s no strict mode in sight, nothing will throw exceptions, and what worked before will still work:

function fun()
{
  assertEq(fun.caller, null); // global scope
  assertEq(fun.arguments, arguments);
  assertEq(arguments.callee, fun);
  arguments.caller; // won't throw, won't do anything special
  return arguments;
}
fun();

Conclusion

With these changes, which are required by ES5, stack inspection is slowly going the way of the dodo. Don’t use it or rely on it! Even if you never use strict mode, beware the third change, for it still might affect you if you provide methods for other code to call. (But don’t expect to be able to avoid strict mode forever: I expect all JavaScript libraries will adopt strict mode in short order, given its benefits.)

(For those curious about new Error().stack, we’re still considering what to do about it. Regrettably, we may need to kill it for information-privacy reasons too, or at least censor it to something less useful. Nothing’s certain yet; stay tuned for further announcements should we make changes.)

You can experiment with a version of Firefox with these changes by downloading a TraceMonkey branch nightly; these changes should also make their way into mozilla-central nightlies shortly, if you’d rather stick to trunk builds. (Don’t forget to use the profile manager if you want to keep the settings you use with your primary Firefox installation pristine.)

10 Comments »

  1. Too bad, this patterns was quite neat:

      window.addEventListener("load", function()
      {
        window.removeEventListener("load", arguments.callee, false);
        // Do something here
      }, false);
    

    Now this event listener will need to be named. On the bright side – looking through the Adblock Plus code I found only one instance using callee/caller properties and none retrieving arguments via function name. So I only have to name one event handler.

    I guess Components.stack will work as usually except that it won’t show some frames due to optimization?

    Comment by Wladimir Palant — 09.09.10 @ 00:52

  2. Wow, I suddenly realize why some developers have a strong reaction to Firebug source code: we use all of these features a lot! This ‘strict’ thing is really old-school. We need is better support for dynamic features, not ways to turn Javascript in to Java or Pascal.

    Comment by johnjbarton — 09.09.10 @ 08:02

  3. Please, please keep me posted on Error.stack. I originally proposed the idea for Mozilla Application Suite many, many years ago, and I rely on it a lot.

    If there’s a Bugzilla bug filed for this, or a newsgroup discussion, I’d like to be cc’d on it. My e-mail address and Bugzilla log-in are the same.

    Personally, I don’t mind one bit the other changes mentioned here.

    Comment by Alex Vincent — 09.09.10 @ 08:40

  4. At the moment we don’t actually do any of those hyper-optimizations that strict mode permits with respect to inlining and such, so Components.stack always reflects exactly what’s happened. That might change at some time, or it might not. I can say with confidence it won’t be changing in Firefox 4, tho. :-)

    Comment by Jeff — 09.09.10 @ 09:20

  5. How, and why, do you rely on Error.stack? I can’t think of a single case where I’ve used it for anything except quick debugging without a debugger (which is admittedly nice, but not so nice that applications should start to rely on it, particularly if in the future it starts to lie [due to optimizations] or is censored [for privacy reasons]).

    Comment by Jeff — 09.09.10 @ 09:28

  6. [...] она дает может прочитать на английском, например, вот тут. Так вот у меня есть предложение ввести еще и "use Occam's [...]

    Pingback by ES5: "use Occam's razor"; | CTAPbIu_MABP's BLOG — 13.09.10 @ 04:53

  7. stack is essential, my extension for example logs errors and warnings complete with stack info internally, and users can choose to include this info in bug reports. Removing this would make it harder to fix bugs.

    Comment by Matty — 22.09.10 @ 09:15

  8. That’s entirely possible, that bugs would become trickier to diagnose.

    Nevertheless, I don’t think, if we are truly concerned about language integrity, that we can preserve nice-to-have-but-not-essential debugging features. (A possible better option: make debugging easier.) And in any case, stack hasn’t yet been changed in any way and quite possibly may not be changed, so an excess of worry would be premature.

    Comment by Jeff — 27.09.10 @ 23:25

  9. [...] New ES5 strict mode support, see the restrictions [...]

    Pingback by Firefox 4: recent changes in Firefox ✩ Mozilla Hacks – the Web developer blog — 06.10.10 @ 14:06

  10. [...] 새 엄격한 ES5 모드(ES5 strict mode) 지원, 제한 사항 보기 [...]

    Pingback by Firefox 4 : 파이어폭스에서 최근 바뀐 점 ✩ Mozilla 웹 기술 블로그 — 09.12.10 @ 06:15

RSS feed for comments on this post. TrackBack URI

Leave a comment

HTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>