Breaking changes in let and const in Firefox Nightly 44

TL;DR

Many add-ons are currently broken on Nightly (Firefox 44) due to some changes that were done in the way let and const behave. These changes were introduced to make Firefox compliant with the final ES6 standard. These changes can lead to JS errors that break your code entirely, so we suggest you test your add-ons extensively to make sure they continue to work.

All add-ons built with JPM (except the latest version) are currently affected by this. We plan to automatically repack all affected JPM add-ons on AMO, but encourage you to repackage the add-on yourself and save some time. Pre-JPM versions of the SDK aren’t affected.

Update: additionally, with the change in bug 1167029, let blocks (like let (x = 42) { use(x); } ) will also stop working.

Please read on for the details of this change, written by Shu-yu Guo. It’s an interesting read even if your add-on isn’t affected.


SpiderMonkey has had non-standard let and const bindings for years. Recently we updated the semantics of let and const bindings for the global level to be compliant with ES6 semantics. ES6 semantics is not compatible with SpiderMonkey’s legacy semantics. (For an introduction to ES6 semantics, please refer to Jason Orendorff’s post.)

Did this update break your add-on? This post will help you diagnose and fix issues it caused.

Legacy Semantics

At the global level, legacy let was equivalent to var. Inside the parser, it was in fact parsed as if the token were var.

Global-level legacy const was like var, except that the property it introduced was read-only.

ES6 global lexical bindings are not properties

The biggest incompatibility is that ES6 let and const bindings, unlike their legacy counterparts, are no longer properties on the global object. Instead, they are bindings in the global lexical scope directly below the global object.

For example,

const x = 42;
// Prints false for ES6, true for legacy
dump('x' in this);

Many add-ons have the expectation that global let and const introduce properties. For instance, a legacy JSM might define constants and globals:

// Foo.jsm
const MY_CONSTANT = 42;
let gFoo = "foo";
 
// addon.js
var FooModule = Cu.import("Foo.jsm", {});
dump(FooModule.MY_CONSTANT);
dump(FooModule.gFoo);

With ES6 const, FooModule.MY_CONSTANT and FooModule.gFoo are both undefined, because their bindings are in a separate scope and not properties on the global object. This makes bugs caused by these errors particularly elusive.

For uses of global legacy let bindings that need to be accessed as properties, I recommend declaring them with var.

Unfortunately, there is no ES6 syntax with the same semantics as legacy const. If the read-only aspect of the property is necessary, I recommend manually defining a property on the global object:

Object.defineProperty(globalObject, "MY_CONSTANT", {
  value: 42,
  enumerable: true,
  writable: false
});

If your add-on imports XPCOMUtils, XPCOMUtils.defineConstant(obj, key, value) does exactly that.

ES6 global lexical bindings may not be redeclared

In ES6, global lexical bindings may not be redeclared in any way: not by let, const, nor var. For example, if the global level has let foo or const foo, any subsequent occurrences of let foo, const foo, or var foo will throw.

Redeclaration errors are easy to fix: rename the variable or remove the declarator and assign to the already-declared variable directly.

ES6 global lexical bindings have TDZ

In ES6, no lexical binding may be used before its declaration is reached. For example, the following throws:

dump(x);
let x;

This has long been regarded as poor style in the community, and fortunately, such errors in existing code are rare. If such an error is encountered, it is very likely a bug in the code.

Global lexical scope and JSSubScriptLoader

The subscript loader may load new scripts into the global scope. This interacts with the ES6 global lexical scope. The pitfall is that since lexical bindings may not be redeclared, loading multiple scripts that redeclare globals with let or const now result in error. For example,

// foo.js
let gFoo;
 
// bar.js
let gFoo;
 
// addon.js
loader.loadSubScript("foo.js");
loader.loadSubScript("bar.js"); // throws due to redeclaration of gFoo

Global lexical scope and the component loader

When loading components, such as via Cu.import, each component has its own global scope and global lexical scope, so cross-script redeclaration issues do not arise.

Cu.import returns the global object of the imported component, so the main pitfall is using let and const-declared bindings as properties on that scope.

Global lexical scope and custom scopes

Both the subscript and component loaders let users load scripts whose global variables and properties are stored in a user-provided scope object. For example, suppose we had foo.js:

// foo.js
var gVar;
let gLet;
const gConst;

Calling loader.loadSubScript("foo.js", myScope) would result in parsing foo.js with the following scope chain, from outermost to innermost:

     Global object
           |
  Global lexical scope
           |
        myScope
           |
          ...

Without user-passed scopes, var bindings go on the global object. Lexical let and const bindings go on the global lexical scope.

From the point of view of var bindings, myScope behaves like the global object: they capture var bindings as properties. That is, gVar is a property on myScope, not the global object.

Global lexical let and const bindings shadow global properties: gLet would hide a property reachable via this.gLet. Since myScope captures var bindings, consistency requires myScope to have its own lexical scope that captures let and const bindings:

     Global object
           |
  Global lexical scope
           |
        myScope
           |
  myScope's lexical scope

In the example, gLet and gConst are bindings in myScope‘s lexical scope. Multiple scripts loaded into myScope would get the same lexical scope. Scripts loaded into myOtherScope would get myOtherScope‘s lexical scope, an entirely different scope.

Note that lexical bindings are still not properties on the non-syntactic scope. If your add-on uses custom scopes, you may run into the problems described in “ES6 global lexical bindings are not properties” above.

9 responses

  1. Noitidart wrote on :

    +1

    I’ve been wondering what the differences were. @lithopsian gave us some good info too on specific cases he’s seen common in firefox addons:

    http://forums.mozillazine.org/viewtopic.php?f=19&t=2967155&sid=1608727908f1b944170144bd1f705da6

  2. Vangelis wrote on :

    Hello Jorge 🙂

    This latest change broke one of my most used addons:

    https://addons.mozilla.org/el/firefox/addon/proxy-selector/

    the author of which is difficult to be contacted…
    I’m trying to possibly fix this myself,
    but since I don’t know JS code, I’d kindly ask for help…

    The feature broken is the ability to auto-reload tab
    (if pref extensions.proxyselector.reload.tab is true)
    when a different proxy is selected; that one broke
    with Nightly 44.0a1 build ID 20151008030232.

    The reload.tab code inside file proxy.js reads:

    // Reload Page
    if(typeof(gBrowser) != ‘undefined’ && prxSel.Prefs.getBoolPref(“extensions.proxyselector.reload.tab”)){
    try{
    var oTab = gBrowser.mCurrentBrowser;
    oTab.webNavigation.reload(nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
    } catch(err) { }
    }

    I see no let or const there (to change into var),
    why is it broken?

    Many thanks for any assistance offered
    (by anyone savvy…)

    Regards,
    Vangelis.

    1. Jorge Villalobos wrote on :

      I haven’t tested this myself, but it’s likely that the add-on broke due to other changes in Nightly instead of this particular one. Or it could be that some other code broke because of this change and still affects that feature. Checking the Browser Console might give you some hints.

      You can also try looking for help in the Add-ons Forum.

      1. Vangelis wrote on :

        @Jorge:

        Many thanks for your reply; I understand this is not
        the right place to seek addon support, however that
        specific feature of the addon was working with
        Nightly 44.0a1 Build ID 20151007030205, but broke
        with Build ID 20151008030232, which contains all the commits
        pushed by Shu-yu Guo that relate to Bugs 589199 & 1202902…

        For now, the core functions of the addon are working,
        I just have to reload tab manually; after all, testing Nightly
        for more than two years, I’m used to things breakin’ up…

        I shall follow your advice and take this to Add-ons Forum
        or even Mozillazine.org forums…

        Again, thanks!

  3. kup wrote on :

    Hello, Vangelis!

    If you’re absolutelly shure, that this part of code has a real problem, you can change a ‘catch’ block to see, what kind of bug it has. Something like this:

    catch(err)
    {
    console.log(‘EXCEPTION THROWN AT: ‘ + err);
    }

    As Jorge said, go then to the browser’s console and look at “EXCEPTION THROWN AT: ” string.

    1. Vangelis wrote on :

      @kup

      Many thanks for your contribution!
      As stated, I am but a mere user / Nightly tester
      with absolutely no coding skills; debugging
      JS code is really beyond my expertise… 🙁

      I took up your suggestion (though an initial
      copy/paste of your code did not work, because
      the blog software somehow turned what were
      meant to be “single” quotes into “inverted” commas)
      and Browser Console produced the following entry:

      EXCEPTION THROWN AT: ReferenceError: nsIWebNavigation is not defined proxy.js:672:22

      If that makes any sense to you and
      you’re able to pin-point the offending code,
      hopefully leading to you posting a fix,
      then by all means please do (I’ll be forever indebted to you),
      else I wouldn’t want to “litter” Jorge’s blog post
      any further with something that may or may not be
      related to the “let and const” changes…

      Спасибо

      1. kup wrote on :

        First of all, I want to apologize to Jorge and other guys for “trashing” this blog from my side. Then, as far as I can understand, “nsIWebNavigation” interface already contains CONST (description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWebNavigation). But, not really sure.

        You can try to write something like this:

        // code block starts
        try
        {
        //write THIS-BELOW string to define “nsIWebNavigation” interface
        var nsIWebNavigation = Components.interfaces.nsIWebNavigation;

        var oTab = gBrowser.mCurrentBrowser;
        oTab.webNavigation.reload(nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
        }
        catch(err)
        {
        console.log(‘EXCEPTION THROWN AT: ‘ + err);
        }
        // code block ends

        OR

        // code block starts
        try
        {
        //write THIS-BELOW string to define “nsIWebNavigation” interface
        var nsIWebNavigation = Components.interfaces.nsIWebNavigation;

        var oTab = gBrowser.mCurrentBrowser;

        //change “webNavigation.reload” with “reloadWithFlags”
        oTab.reloadWithFlags(nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
        }
        catch(err)
        {
        console.log(‘EXCEPTION THROWN AT: ‘ + err);
        }
        // code block ends

        Hope, this will work for you. Sorry, I can’t test it by myself because of missing a testing FF build (simply, I do not have it).

        1. Vangelis wrote on :

          kup, you are worth your weight in gold
          (OK, I’m simply exaggerating, but my
          gratitude towards you is immense)!

          Actually, BOTH of your suggestions do WORK
          as intended, i.e. after the addon changes proxy
          and wipes out cookies, it auto-reloads active tab (Y).

          For anyone keeping track of this,
          I’ve documented it at:

          http://pastie.org/10500901

          It appears the addon developer can only be
          contacted in the AMO review section (when and
          if he reads those); I’ll leave it up to the real
          Javascript experts to figure out why
          the code was impacted by the recent changes (???).

          kup, a 1,000,000 thanks!

  4. kup wrote on :

    Vangelis, don’t mention it! I’m glad, that my solution finally helps you, and it works fine. What about tracking fixes at pastie.org – it’s really good idea, you’re the man =) !