Skip To Main Content

The ES2015 Nightmarefile

Posted by Mike Pennisi

Jun 16 2015

They tried to cover this up. In designing ECMAScript 2015 (a.k.a. ES6, a.k.a. ES2015), the authors identified a number of undesirable side effects of their work. “Why worry?” they asked. “People will be so smitten with arrow functions and block-scope bindings that they won’t care about a few measly backwards-breaking changes.” Well I care, and I have evidence that suggests I’m not alone.

Redatcted Debugger

Through an extensive survey of JavaScript across the web, I have found a file that demonstrates these hazardous changes to the language. This code, published in 2009 to a pop singer’s fan site, describes a program that is 100% compliant with the ECMAScript 5 standard. The web site will not function as intended for much longer–indeed, many of today’s JavaScript engines have already implemented new language features that interfere with its original behavior.

Needless to say, this is the least glamorous aspect of the new language, and I’m sure there are plenty of people who would rather it go unnoticed. Publicizing this information won’t earn me any friends, but I trust that history will judge me favorably.

I’ll include the source in its entirety then step through piece-by-piece to call out the increasingly-broken aspects. Finally (and most importantly) I’ll share some thoughts on what all this means for the web as a platform.

The File

$(document).ready(function() {
  // set the date for midnight according to the user's clock
  var LastSongRelease = new Date("2010-03-31T00:00:00");
  LastSongRelease.setTime(
    LastSongRelease.getTimezoneOffset() * 60 * 1000 + LastSongRelease
  );
  // "let" for "L.ast Song E.xclamation T.ext"
  var let = [];
  var first = 0, second = 1, third = 2;
  {
    let[first] = "omg it's out";
    let[second] = "in theaters now";
    let[third] = "the wait is over";
  }
  let = let.join("?").toUpperCase().split("?");

  var SlideShow = Object.getOwnPropertyDescriptor({
    get ctor() {
      var $slides = $("#slides .miley-photo");
      var current = 0;

      this.go = function(change) {
        current = (current + change) % $slides.length;

        for (var i = 0 in $slides.toArray()) {
          if (i === current === delete function() {}.length) {
            $slides.eq(i).hide();
          } else {
            $slides.eq(i).show();
          }
        }
      };
    }
  }, "ctor").get;
  var slideShow = new SlideShow();

  $("div#next-pic").bind("click", function() { slideShow.go(1); });
  $("div#prev-pic").bind("click", function() { slideShow.go(-1); });

  // update the countdown timer
  setInterval(function() {
    var milliseconds = LastSongRelease - Date.now();
    var oldContent = $("#countdown").html();
    var newContent = milliseconds < Number.prototype ?
      "<b>" + let[-milliseconds % 3] + "!!!</b>" :
      milliseconds + " milliseconds until <i>The Last Song</i> comes out!";

    // only update if the text has changed
    var updateDetector = "({ get " + JSON.stringify(oldContent) +
      "() {}, get " + JSON.stringify(newContent) + "() {} })";
    try {
      eval(updateDetector);
    } catch(err) {
      var err;
      if (!(err instanceof SyntaxError)) {
        throw err;
      }
      return;
    }

    $("#countdown").html(newContent);
  }, 500);
});

Explanations

It’s just 63 lines of code, but I count nine distinct features that will break in ES6. Let’s get started.

// set the date for midnight according to the user's clock
var LastSongRelease = new Date("2010-03-31T00:00:00");
LastSongRelease.setTime(
  LastSongRelease.getTimezoneOffset() * 60 * 1000 + LastSongRelease
);

Here, a Date instance is being created from a ISO 8601-formatted string. Crucially, no time zone offset is specified. According to ES5, “The value of an absent time zone offset is ‘Z’.” That’s why the author needed to explicitly correct for the local clock as configured on the user’s computer. ES6, on the other hand, states, “If the time zone offset is absent, the date-time is interpreted as a local time.” In compliant engines, the “correction” is unnecessary, making the target time incorrect. This means any Date initialized to compensate for the user’s local time zone in this way will be wrong.

var let = [];
var first = 0, second = 1, third = 2;
{
  let[first] = "omg it's out";
  let[second] = "in theaters now";
  let[third] = "the wait is over";
}

In ES5, let is a valid identifier outside of strict mode. While it remains a valid identifier in ES6, the new destructuring assignment syntax takes precedence here. ES6 interprets the token sequence let [ as the beginning of a destructuring assignment. Instead of defining numeric properties on the array stored in let, the final three statements will define new block-scoped bindings for first, second, and third (using the values "o", "i", and "t" respectively).

let = let.join("?").toUpperCase().split("?");

Note the use of a Deseret character (specifically: lower case “long I”) as a string separator. In order to restore the original array, this snippet relies on the fact that ES5’s String.prototype.toUpperCase does not honor surrogate pairs, so the transformed string still contains the same Deseret characters. ES6, on the other hand, requires that implementors derive the result according to case mappings. The transformed string will no longer contain any instances of lower case “long I”, and the resultant array will only have a single element: a string representing the original three values joined by an upper case version of the separator.

I’ll admit: the Deseret lower case “long I” is a somewhat uncommon choice for a separator, but the implications of this change are no less disturbing.

var SlideShow = Object.getOwnPropertyDescriptor({
  get ctor() {
    // (we'll get to this code in a moment)
  }
}, "ctor").get;
var slideShow = new SlideShow();

In ES5, functions declared as “accessor properties” like these are just normal functions. In ES6, they are special “method” functions that do not define a prototype and cannot be used as a constructor. The definition of SlideShow is not in itself threatened, but ES6 environments will generate an error when that function is invoked with new.

That’s right, all code written in this way will suddenly produce new runtime errors! Sure, I’ve never in my life seen a constructor defined like this, but who am I to judge?

for (var i = 0 in slides.toArray()) {
  // (we'll get to this code in a moment)
}

Although ES5 allows for an Initialiser to be used here, the semantics of the for..in statement make it completely unnecessary. ES6 defines the grammar more strictly, so the same code will be rejected by compliant parsers. Picture the surprise of the author of this code when this stops working! Well, okay, with this one they will probably be more surprised that it worked in the first place. There’s more, though!

if (i === current === delete function() {}.length) {
  $slides.eq(i).hide();
} else {
  $slides.eq(i).show();
}

ES5 states that the length property of Function instances is not configurable, so the delete operation returns false and the branch is only executed when i and current are not strictly equal. That property is in fact configurable in ES6, so the behavior of this code is inverted–the “current” image will be hidden, and all others will be displayed.

Across the web, code that uses the expression delete function() {}.length instead of the literal false will suddenly start behaving in surprising ways.

This honestly seemed more catastrophic when I first started writing this post.

var newContent = milliseconds < Number.prototype ?
  "<b>" + let[-milliseconds % 3] + "!!!</b>" :
  milliseconds + " milliseconds until <i>The Last Song</i> comes out!";

This code depends on the fact that ES5 defines the Number prototype as “a Number object […] whose value is +0”. Useful as that may be, it is no longer true in ES6. The spec is very clear on this: “The Number prototype object is an ordinary object. It is not a Number instance”. In an ES6 engine, the condition in the above ternary will always evaluate to false, meaning (now that The Last Song has been released) the countdown timer will be updated with an increasingly large negative value.

Can you imagine the confusion of all those visitors when the countdown starts counting negative time? All because the developer innocently used Number.prototype instead of… well, instead of 0, which I suppose might be the more natural/common choice here.

// only update if the text has changed
var updateDetector = "({ get " + JSON.stringify(oldContent) +
  "() {}, get " + JSON.stringify(newContent) + "() {} })";
try {
  eval(updateDetector);
} catch(err) {
  // (we'll get to this code in a moment)
}

While ES5 explicitly forbids duplicate property names in object initializers, ES6 makes no such restriction.

Now here is a truly hazardous change! I mean, is it really wise to break the untold millions of scripts that compare strings by evaluating carefully-escaped representations inserted into an object literal definition and dynamically evaluated, branching on the presence of a SyntaxError?

Written out like that, this incompatibility doesn’t seem so bad. Let’s just forget this one.

try {
  // (see above)
} catch(err) {
  var err;
  if (!(err instanceof SyntaxError)) {
    throw err;
  }
  return;
}

The semantics for catch blocks in ECMAScript 5 are a little tricky. ES5 first copies the surrounding lexical environment, and it then inserts a new binding for the identifier specified within the parenthesis that follow the catch token. Notably, ES5 makes no special restrictions on variable declarations within the catch block itself. ES6, on the other hand, explicitly forbids re-defining the binding for the “caught” error. This snippet, which runs just fine in Internet Explorer 8, will not parse in ES6-compliant engines.

It probably goes without saying that the var declaration is totally unnecessary in this case. I’m going to level with you: I’m having second thoughts about the severity of all these conflicts.

Reflections

Okay, so maybe I laid it on a bit thick in the intro. Given how bizarre most of these examples are, it’s difficult to argue for the severity of any of the incompatibilities. And I don’t mean “bizarre” in the hoity-toity “I’m a professional developer with standards” sense, either. I mean it in the completely sincere, “I didn’t know some of those things were even possible” sense.

To be clear: the Nightmarefile doesn’t demonstrate all the backwards-breaking changes in ES6. The spec helpfully documents every intentional incompatibility under sections labeled Annex D and Annex E. I think you’ll agree that the other incompatibilities are even more esoteric (and consequently less troubling) than those described above. Annex D and E omit the rationale for the changes (one document can only have so much information, after all), so you may find yourself asking, “Why?” Lucky for us, TC39’s decision making process is publicly documented, so you can usually find rationale for any given design decision (it just might take some digging). For insight into the process, check out the TC39 Meeting Minutes, the ECMAScript bug tracker and the ES-Discuss mailing list archives.

Considering the breadth of the changes, it’s actually amazing that so little has been broken. This is no accident; early in the discussion for ES6, TC39 member Dave Herman coined the term “One JavaScript” to signify the committee’s aversion to “breaking the web.” Every new feature has been vetted against the ES5 specification, and as you can see, only in the rarest of circumstances has existing code been threatened. No news is good news in this case, and TC39 deserves recognition for the extra care this detail requires.

Posted by
Mike Pennisi
on June 16th, 2015

Comments

We moved off of Disqus for data privacy and consent concerns, and are currently searching for a new commenting tool.

Contact Us

We'd love to hear from you. Get in touch!