Sunday, August 30, 2015

Learning ES6: Block-level scoping with let and const

Coming off the heels of discussing arrow functions, let’s continue the Learning ES6 series talking about block-level scoping in ECMAScript 6.

TL;DR

let is the new var. ES6 provides two new ways for declaring variables: let and const. These pretty much replace the ES3 or ES5 way of declaring variables using var. By using block-level scoping, these two keywords help developers avoid common mistakes they make not because they write bad code, but because they don’t fully understand the idiosyncrasies of how JavaScript handles variables.

Let’s take a look at an example:

function simpleExample(value) {
    const constValue = value;

    if (value) {
        var varValue = value;
        let letValue = value;

        console.log('inside block', varValue, letValue);
    }

    console.log('outside block');

    // varValue is available even though it was defined
    // in if-block because it was "hoisted" to function scope
    console.log(varValue);

    try {
        // letValue is a ReferenceError because it
        // was defined w/in if-block
        console.log(letValue);
    }
    catch (e) {
        // e is a ReferenceError
        console.log('letValue not accessible', e);
    }

    // SyntaxError to try and update a variable
    // declared via const
    //constValue += 1;
}

simpleExample(2);

Variables declared via let are not available outside of the block in which they are declared. Variables declared via const cannot be updated. You can find more examples in the block-level scoping code examples for the Learning ES6 Github repo.

You just know you’re interested, so keep on reading!

A quick look at var

Before we jump into let and const, let’s remind ourselves about how var works. In the History of ECMAScript, we learned that Brandon Eich supposedly created JavaScript in 10 days. I still find that hard to believe, but the way var declarations work in JavaScript may very well be the proof that it really was developed that quickly.

Nicholas C. Zakas explains it best in his book Understanding ECMAScript 6:

Traditionally, one of the tricky parts of JavaScript has been the way that var declarations work. In most C-based languages [such as C++, Java or C#], variables are created at the spot where the declaration occurs. In JavaScript, however, this is not the case. Variables declared using var are hoisted to the top of the function (or global scope) regardless of where the actual declaration occurs.

Most of the time we don’t run into any problems with var, but when we accidentally do, the resulting bugs can be hair-pulling:

function varExample() {
    var myVar = 7;

    console.log('myVar after declaration', myVar);

    // even though laterVar is defined later on in the function
    // it is "hoisted" to the beginning of the function &
    // initialized to undefined. In most C-style languages this would
    // be an error.
    console.log('laterVar before declaration', laterVar);

    laterVar = 10;

    // image some legitimate conditional
    if (myVar < 20) {
        // accidental redefintion of myVar results
        // in outer defined myVar being reassigned
        // to 'foo'
        var myVar = 'foo';
        var innerVar = true;

        console.log('myVar inside block', myVar);
    }

    // since this declaration was "hoisted", it's as if it's no
    // longer here but at the top of the function
    var laterVar;

    // looking at the code laterVar *should* be undefined,
    // but it has the value 10 from earlier
    console.log('laterVar after declaration', laterVar);

    // we would expect myVar to still be 7
    // but it was redefined and overwritten
    // w/in the conditional
    console.log('myVar outside block', myVar === 7);

    // we would expect innerVar to no longer be accessible
    // since it was defined w/in the if-block, but it was
    // "hoisted" as well
    console.log('innerVar outside block', innerVar);
}
varExample();

You can imagine if your function was more complicated how you could accidentally re-declare your variables and be confused by why the function was misbehaving. This is why there are JSHint and ESLint rules that all var declarations must be at the top of the function.

let is the new var

let works similarly to var, but the variable it declares is block-scoped; it only exists within the current block.

function letExample(value) {
    if (value) {
        let letValue = value;

        console.log('inside block', letValue);

        // redeclaration of letValue would be a SyntaxError
        let letValue = 'foo';
    }

    try {
        // Accessing letValue is a ReferenceError because it
        // was defined w/in if-block
        console.log(letValue);

        // if we get here, it means that the JS engine didn't
        // throw an exception, which means that the engine
        // (or transpiled code) did not faithfully reproduce
        // how let should work
        console.log('let not faithfully handled');
    }
    catch (e) {
        // e is a ReferenceError
        console.log('letValue not accessible', e);
    }
}
letExample(2);

As you can see it’s a ReferenceError if you try to access a variable outside of the block in which it was declared. With var we would’ve received undefined. Also redeclaring a let variable is a TypeError. With a var declaration you would get no such warning. In a nutshell, let works how you probably thought var worked.

Transpiled let code

When let declarations are transpiled down to ES5, they are basically converted to var declarations. If you tried to transpile the code above but with let letValue = 'foo'; uncommented, both Babel and TypeScript throw compilation errors. They won’t even transpile the code because of the redeclaration.

However, accessing letValue out of block scope is a different story. The transpiled Babel code changes all of the appropriate uses of letValue to _letValue. The result is that there is still a ReferenceError when accessing letValue within the try-catch block even with the transpiled code using var declarations. Traceur unfortunately is not as robust resulting in the console.log('let not faithfully handled'); line actually being executed. I’m wondering if the try-catch is somehow throwing Traceur off. Something to keep in mind when choosing a transpiler.

Shadowing variables with let

We saw in the varExample earlier that when you redeclare a variable with var in a nested scope (such as an if-block), the variable isn’t actually redeclared. Since the variables had the same name, the second declaration just resulted in the variable’s value being reassigned. This isn’t the case with let:

function letShadowExample() {
    let x = 15;

    if (true) {
        // this x "shadows" the x defined in the outer scope.
        // this new x just exists within the scope of the
        // if-block
        let x = 21;

        // x should be 21
        console.log('x inner block', x);
    }

    // x should be 15
    console.log('x outer block', x);
}
letShadowExample();

Within the nested scope of the if-block, the let declaration of x is different than that of the outer scope. Now hopefully you wouldn’t write code like this because it’s very confusing, but at least it now works like how other major programming languages work. Both Babel and Traceur rename the nested x variable to something different so that when the code is transpiled to ES5 and using var declarations, the variables are treated differently.

Keeping things const

A const declaration works much like let except you must initialize the variable immediately with a value. And that value cannot be changed afterwards. You will get a SyntaxError if you either fail to initialize the variable at declaration or if you try to reassign its value. Let’s take a look at a quick example:

function constExample() {
    const NAME_KEY = 'name';
    const UNFROZEN_OBJ_CONST = { key: 'adam', val: 'eve' };
    const FROZEN_OBJ_CONST = Object.freeze({ key: 'jesus', val: 'paul' });

    // All const declarations must be initialized.
    // It's a SyntaxError otherwise
    const VALUE_KEY;

    // Const variables are read-only, so trying to
    // reassign is a SyntaxError too
    NAME_KEY = 'key';

    // GOTCHA: even though the object is const, you can still
    // change properties of it. It's the variable
    // that cannot be reassigned
    UNFROZEN_OBJ_CONST.key = 'moses';

    // by freezing the object, using ES5 Object.freeze
    // its properties cannot be changed.
    // in strict mode this a TypeError. In non-strict
    // mode the value silently doesn't change
    FROZEN_OBJ_CONST.val = 'peter';

    console.log('const value', NAME_KEY);
    console.log('unfrozen object', UNFROZEN_OBJ_CONST);
    console.log('frozen object', FROZEN_OBJ_CONST);
}
constExample();

As shown in the code, a variable declared via const means that it cannot be a reassigned, but it does not mean that its contents cannot be changed when it is an object. We can somewhat fix this problem (if it is one), by using the Object.freeze method we got from ES5.

Entering the Temporal Dead Zone

The temporal dead zone (TDZ) is just a fancy term used for the time period where code execution is in the scope of a variable declared by let or const, but before it is actually declared. The variable is in scope, but not yet initialized. Accessing an uninitialized variable is a ReferenceError. Let’s take a look at some example code:

{
    // Uninitialized “binding” for `disciple` variable is created
    // upon entering scope. TDZ for `disciple` variable begins

    // accessing a variable in TDZ either to get or set
    // is a ReferenceError
    disciple = ‘matthew’;
    console.log(disciple);

    // TDZ ends at declaration and `disciple` is initialized
    // w/ `undefined` value
    let disciple;

    console.log(disciple); // undefined

    disciple = ‘thomas’;
    console.log(disciple); // ‘thomas’
}

So why is it called the temporal dead zone? It’s because the dead zone is based on the period of code execution time versus where the code actually resides:

function temporalDeadZoneExample() {
    // TDZ for `value` begins

    const func = function() {
        // Even though this function is defined *before*
        // `value` in the code, it's not called until after
        // `value` is declared, so accessing it is OK.
        console.log('value is: ', value);
    }

    // TDZ for `value` continues. Accessing `value`
    // here would be a ReferenceError. Calling `func`
    // here would cause a ReferenceError.

    // TDZ ends with declaration of `value`
    let value = 'foo';

    // no longer in TDZ when calling function so now
    // any access of `value` is ok
    func();
}
temporalDeadZoneExample();

Variables declared by var don’t have a TDZ because the variables are “hoisted” to the beginning of functions. Therefore they are always declared as well as initialized with a value of undefined.

let and loops

Unbeknownst to most JavaScript developers, the iteration variable declared with var within the head of for-loops (such as for (var i = 0; i < 5; i++)) is available outside of the for-loop. Because of block-level scoping, with let this is no longer the case.

function simpleLoopExample () {
    for (var i = 0; i < 5; i++) {
        console.log('i=', i);
    }
    for (let j = 0; j < 5; j++) {
        console.log('j=', j);
    }

    // i is accessible outside of the for loop
    // and has the value 5
    console.log('after i=', i);

    // j is not accessible outside of the for loop
    // and is a ReferenceError
    console.log('after j=', j);
}
simpleLoopExample();

In practice, this typically is not a problem because we would rarely try to access a loop iteration variable outside of a for-loop. However, this issue can crop up when newbie JavaScript developers create callback functions within loops.

function callbackLoopVarExample() {
    var $body = $('body');

    for (var i = 0; i < 5; i++) {
        // create 5 buttons with the index in the name
        var $button = $('<button>var ' + i + '</button>');

        // wire click handler w/ callback using arrow function!
        $button.click(
            // BUG! When button is clicked, the value of `i` is 5!
            () => console.log('var button ' + i + ' clicked!')
        );

        // add button to the body
        $body.append($button);
    }
}
callbackLoopVarExample();

For those not too familiar with JavaScript development, it may not be immediately apparent why the console.log message always has 'var button 5 clicked!'. Because the i variable is “hoisted” to the top of the function it still has a value after the for loop has ended. That value is 5, which is what caused the termination of the loop. And since i is scoped to the whole function, all of the callback functions are bound to the same i, resulting in them all displaying 'var button 5 clicked!'.

The ES3/ES5 way of solving this problem was to use a separate named function or an IIFE that would create a new scope for the iteration variable such that each callback function would be bound to its own version. Here’s an example:

function callbackLoopNamedFunctionExample() {
    var $body = $('body');

    // Create a named function passing in the loop iteration variable
    // which creates a unique scope for each iteration so
    // that the callback function binds to its own variable.
    var loop = function(index) {
        // create 5 buttons with the index in the name
        var $button = $('<button>function ' + index + '</button>');

        // wire click handler w/ callback using arrow function!
        $button.click(
            // Fixed! `index` is unique per iteration
            () => console.log('function button ' + index + ' clicked!')
        );

        // add button to the body
        $body.append($button);
    }

    for (var i = 0; i < 5; i++) {
        loop(i);
    } 
}
callbackLoopNamedFunctionExample();

Now when we click each button, the appropriate message is displayed. This problem could have also been solved by having an IIFE defined within the for-loop in much the way our loop function variable was defined. The need for this sort of workaround goes away when declaring the iteration variable via let:

function callbackLoopLetExample() {
    let $body = $('body');

    for (let i = 0; i < 5; i++) {
        // create 5 buttons with the index in the name
        let $button = $('<button>let ' + i + '</button>');

        // wire click handler w/ callback using arrow function!
        $button.click(
            // Fixed! `i` is a different variable declaration for
            // each iteration of the loop as one would expect!
            () => console.log('let button ' + i + ' clicked!')
        );

        // add button to the body
        $body.append($button);
    }
}
callbackLoopLetExample();

The key here is using let for the iteration variable. The i variable is now a new declaration for each iteration of the loop, resulting in the callback function having its own i variable. Once again, things work with let as we would’ve expected them with var.

One thing to note is that both Babel & Traceur, when they notice this issue, use the named function approach when transpiling the ES6 code down to ES5. This means that if you have a bug in your code, the structure of the transpiled code will look dramatically different than that of your ES6 code. As long as you have a source map in your transpiled code and your ES6 code is also accessible, any line numbers provided by the engine should point you back to the write place in your ES6 code.

Final note on loops. Variables declared by let work the same way with for-in loops as well. ECMAScript 6 added a new type of loop, the for-of loop that works with iterators (also added w/ ES6), but we’ll talk about those in a later article.

Working with parameters

Declaring a variable with let with the same name as a function parameter is a TypeError:

function sellFruits(fruits) {
    let fruits = [];
}

However, if that let declaration happens within a nested scope (such as an if-block), then the variable will be shadowed:

function sellFruits(fruits) {
    // create a simple code block
    {
        // this let declaration of `fruits` shadows the
        // `fruits` parameter
        let fruits = [];

        console.log(values); // []
    }

    // `fruits` here is the parameter value
    console.log(fruits);
}

And like all of the examples prior, var does not act this way. When a parameter is redeclared using var with the same, whether at the top-level function scope or within a nested block, nothing happens. Due to var declarations being functioned scoped, it’s as if those declarations weren’t even there because the parameter has already declared the variable in the scope.

function sellFruits(fruits) {
    // this declaration does nothing
    var fruits;

    // create simple code block
    {
        // the declaration does nothing, but the assignment
        // does assign the parameter value to []
        var fruits = [];

        console.log(fruits); // []
    }

    // `fruits` here is still [] from the assignment
    // in the block
    console.log(fruits);
}

var vs let vs const

Now that we know how let and const work, when should we use them in place of var? Here are some suggestions:

  • Use const for variables you want to be immutable. This works best for primitive values (like Number, String, Boolean, etc). You can use const for objects, but you should probably use Object.freeze in concert to make the object truly immutable. You could use const for a mutable object, but that defeats the “spirit” of const.
  • Use let for the mutable variables (i.e. everything else)
  • The only time you may need to still use var is for objects in the global scope, particularly ES3- and ES5-style namespaces or modules. Ideally you would convert those to ES6-style modules, but for backwards compatibility you may still need to use var.
  • Do not mix and match let and var in a file. Be consistent, otherwise it’ll lead to even more confusion.
  • Do not do a global search and replace of var for let. You may have code that is unintentionally relying on the quirkiness of var. You should do the conversion manually one file at a time.

JavaScript engine support

According to the ECMAScript 6 compatibility table, the following JavaScript engines support let and const:

  • Babel
  • Traceur
  • TypeScript
  • Edge
  • Chrome (with experimental flag enabled and in strict mode)
  • Firefox (code blocks must be wrapped in <script type="application/javascript;version=1.7"> tag)
  • Opera (in strict mode)
  • Node.js / io.js (with flags)

The most notable missing engine is Safari. Of course IE 11 and lower do not support any ES6 features. In order to have the engine faithfully support let and const, particularly the temporal dead zone, you should ensure your scripts are running in strict mode.

Additional resources

You can check out the Learning ES6 examples page for the Learning ES6 Github repo where you will find all of the code used in this article running natively in the browser (for those that support let and const). There are also examples running through Babel and Traceur transpilation.

Other super helpful resources:

Coming up next…

We will be continuing the Learning ES6 series by looking at the fun new destructuring techniques introduced with ES6. Until then…

Sunday, August 23, 2015

Learning ES6: Arrow Functions

After looking at the history of ECMAScript, the goals of ECMAScript 6 and using ES6 right now, the first actual feature we’ll look at in our Learning ES6 series is going to be arrow functions, which are also known as “fat arrow” functions.

TL;DR

Arrow functions are more or less a shorthand form of anonymous function expressions that already exist in JavaScript. In ES6 this looks like:

var squares = [1, 2, 3].map(x => x * x);

Is equivalent to this in ES5:

var squares = [1, 2, 3].map(function (x) { 
    return x * x;
});

As you can see a lot of the verbosity of old-style function expressions is removed and what’s left is the fat arrow (=>) joining the two main ingredients of the function: the arguments and function body.

You’ll find the greatest utility in arrow functions in places where functions take a callback function, like event handlers (such as onClick, $.ajax, etc.) and array processors (such as map, sort, etc.)

Interested in learning about arrow functions in more detail? Well keep on reading then!

Arrow Function Syntax

Arrow functions can have several combinations of syntaxes depending on the needs of the function.

When the arrow function has multiple arguments or no arguments at all, the syntax looks like:

// two or more arguments
var sum = [9, 8, 7].reduce((memo, value) => memo + value, 0);

// no arguments
var getRandom = () => Math.random() * 100;

Notice the parentheses surrounding the arguments. However, when the arrow function has a single argument, the parenthesis can be removed:

var valuesShallowCopy = [‘foo’, ‘bar’].map(x => x);

The full form of an arrow function body supports multiple statements within a block:

$("#deleteButton").click(event => {
    if (confirm(“Are you sure?”)) {
        clearAll();
    }
});

Notice the curly braces surrounding the statement body block. However, when the arrow function only has one statement, the curly braces defining the block can be omitted:

var activeCompanies = companies.filter(company => company.active);

Omitting the curly braces also denotes a single expression which is implicitly returned. You do not need to include the return keyword. However, if you want to return an object, you must wrap that object in parenthesis otherwise it is interpreted as a code block:

// BUG! BUG! BUG!
// will return an array of undefined values since the code block
// is empty and returns nothing
var myObjects = myArray.map(value => {});
console.log(myObjects);

// Correct!
// will return an array of empty objects
var myObjects = myArray.map(puppy => ( {} ) );
console.log(myObjects);

// BUG! BUG! BUG!
// will return an array of undefined values since the code block
// looks like it has a label of “foo” and an expression of “x” that is
// NOT returned.
console.log([4, 5, 1].map(x => {foo: x} ));

// Correct!
// will return an array of objects with “foo” as key and number
// as value
console.log([4, 5, 1].map(x => ( {foo: x} ) ));

You’ll find that arrow functions come in most handy when used as a callback function. The various higher-order functional programming array methods that were introduced with ECMAScript 5 (like map, forEach, reduce, etc.) work well with arrow functions. Arrow functions can also be used as callback functions for event handlers, but typically in an OOP world those end up being (private) methods on your class so that they can be properly unit tested. Arrow functions are not meant for prototype- or class-based methods. We’ll get into why in a bit.

Immediately-invoked arrow functions (IIAFs)

If you recall immediately-invoked function expressions (IIFEs), they allow you to define a function expression and call it immediately in order shield the code from the rest of the program by scoping it within the function. Here’s a simplified example:

(function(message) {
    // print out each character of message
    for (var charNo = 0; charNo < message.length; charNo++) {
        console.log(message.charAt(charNo));
    }
}) (‘hello’);

The same can be done with arrow functions:

( message => {
    // print out each character of message
    for (var charNo = 0; charNo < message.length; charNo++) {
        console.log(message.charAt(charNo));
    }
} ) (‘hello’);

The only thing to be cognizant of is the location of the parenthesis. They have to wrap the arrow function expression before the parenthesis that cause the invocation (before the (‘hello’) part). With IIFEs, the parenthesis could also go around the whole IIFE including the function invocation.

Lexical this

The best thing about arrow functions, aside from the terse syntax, is that this uses lexical scoping; its value is always “inherited” from the enclosing scope.

Let’s look at a JavaScript coding problem with this that should help explain things:

var car = {
    speed: 0,
    accelerate: function() {
        this.accelerator = setInterval(
            function() {
                // BUG! *this* is not what we expect.
                // In non-strict mode, *this* is the
                // global object. In strict mode *this*
                // is undefined. In neither case is *this*
                // the car object we want.
                this.speed++;
                console.log(this.speed);
            },
            100
        );
    },
    cruise: function() {
        clearInterval(this.accelerator);
        console.log('cruising at ' + this.speed + ' mph');
    }
};

car.accelerate();

setTimeout(function() { car.cruise(); }, 5000);

Every newbie JavaScript developer has run into this problem because they didn’t know any better. Every experienced developer has accidentally run into this problem even though they knew better. In ES3 the approach to fix this problem was to store a reference to this in a variable called self, that orm so that it was available in the scope of the anonymous function:

var car = {
    speed: 0,
    accelerate: function() {
        // store a reference to `this` in a variable that will be
        // be available for use within the anonymous function
        // callback
        var self = this;
        this.accelerator = setInterval(
            function() {
                self.speed++;
                console.log(self.speed);
            },
            100
        );
    },
    cruise: function() {
        clearInterval(this.accelerator);
        console.log('cruising at ' + this.speed + ' mph');
    }
};

car.accelerate();

setTimeout(function() { car.cruise(); }, 5000);

Alternatively, with ES5, we could create a new function using the bind method that would pass the desired this to the anonymous function:

var car = {
    speed: 0,
    accelerate: function() {
        this.accelerator = setInterval(
            // bind returns a new “cloned” function
            // such that *this* within the function
            // matches *this* outside of it by passing it
            // as the argument.
            (function() {
                this.speed++;
                console.log(this.speed);
            }).bind(this),
            100
        );
    },
    cruise: function() {
        clearInterval(this.accelerator);
        console.log('cruising at ' + this.speed + ' mph');
    }
};

car.accelerate();

setTimeout(function() { car.cruise(); }, 5000);

With ES6, all of this nonsense is cleaned up nicely because arrow functions have implicit this binding:

var car = {
    speed: 0,
    accelerate: function() {
        this.accelerator = setInterval(
            () => {
                // *this* is the same as it is outside
                // of the arrow function!
                this.speed++;
                console.log(this.speed);
            },
            100
        );
    },
    cruise: function() {
        clearInterval(this.accelerator);
        console.log('cruising at ' + this.speed + ' mph');
    }
};

car.accelerate();

setTimeout(() => car.cruise(), 5000);

Now everyone’s happy. It’s worth noting that transpilers use the ES3 solution for transpiling the lexical this. The only difference is they use an auto-generated variable name (like $__1) instead of self, that or m. They do not use the ES5 bind method, making the transpiled code ES3 compatible. This means that the transpiled code will work in non-ES5 browsers such as IE8.

Identifying arrow functions

Although arrow functions look dramatically different in their syntax and use lexical scoping for this (as well as other constructs), they are still identified as functions. For example, typeof and instanceof both say arrow functions are functions:

console.log(typeof function() { });  // 'function'
console.log(typeof (() => {}));  // 'function'
console.log(function() { } instanceof Function);  // true
console.log((() => {}) instanceof Function);  // true

Lexical arguments

Just like arrow functions do not define their own dynamic this, arrow functions also do not define their own dynamic arguments object either. For instance, you cannot have an arrow function that takes no parameters and then access the arguments object to gain access to those parameters like you can with formal functions.

Instead, the arguments object is “inherited” from the lexical scope of the containing function just like this. That arguments object is then available no matter where the arrow function is executed later on. Let’s take a look at a contrived example:

function genArrowReturningLexArgs() {
    // returns an arrow function expression
    // which itself returns the arguments used
    // when generating the arrow function
    return () => arguments;
}

var arrowFunction = genArrowReturningLexArgs(5, 'foo', [5,4,3]);

// log arguments object with
// 5, 'foo', and [5,4,3]
console.log(arrowFunction());

In order to gain access to the arguments of an arrow function, you either have to name all of the parameters you want access to or use other new ES6 function features like rest parameters. We’ll discuss those in a future article.

Coming up next…

We will be continuing the Learning ES6 series by looking at block-level scoping using the new let and const keywords. Until then…

Saturday, August 15, 2015

20 reasons to drop IE8 like it's hot

R.I.P Internet Explorer 8


Ah, good ol’ IE8. I’m taking a break from the Learning ES6 series to talk about how we should stop supporting the browser that everyone loves to hate: Internet Explorer 8. The bane of our collective existence as web developers. Whether you’re a developer, designer or manager, you know you desperately want to stop supporting it. In this blog post I want to build a case against supporting IE8, examining empirical facts as well as qualitative feelings. My hope is that afterwards you will be well-equipped to make a compelling argument for dropping IE8 like it’s hot.

Despite the playful title of this blog post, this topic is serious business. I feel that we as developers are wasting our time supporting IE8. But there are very legitimate reasons why it’s still supported and why it’s difficult to stop supporting it. So instead of just having a soapbox bash-fest complaining about IE8, let’s instead treat this situation like a court case. We’ll present a case so compelling against IE8 that the judge (aka the boss) will have no choice but to rule in our favor. Cue that Law & Order music!

TL;DR


Microsoft’s Internet Explorer 8 was released way back in 2009 and yet many developers are saddled with the burden of supporting a browser so feature incomplete that it doesn’t support ES5 let alone HTML5, CSS3 or ES6. The main reason IE8 has lasted so long is because it’s the latest version of IE that will run on Microsoft Windows XP, arguably the most popular Windows operating system.

Because web surfers are unable to upgrade their OS, either because it’s personally too costly or their organization prevents them from doing so, many are stuck using IE8 and are unable to upgrade to a newer version of Internet Explorer. However, the latest versions of other browsers like Chrome or Firefox are still available on XP so it is possible for these users to use a modern browser. IE8 traffic is continuing to decline as well. Worldwide traffic across all devices is less than 2%. It would be even lower if the billion users in China were removed.

As any web developer still dealing with IE8 knows, building for, testing with, and debugging on Internet Explorer is pretty difficult. Time spent worrying about IE8 could be better spent building out cool new functionality on the modern browsers that can support it. That’s why sites like Google, The New York Times, SalesForce, and even Microsoft itself are all no longer supporting IE8, choosing to focus on building powerful new web experiences.

These reasons along with additional mounds of evidence should be enough to convince "the powers that be" that IE8 support should be dropped. And while we’re at it, we might as well drop support for IE9 and IE10 as well! They receive even less traffic than IE8.

Intrigued? Want the nitty gritty details? Keep on reading…

Opening Arguments


Any website created within the last couple of years probably didn’t bother worrying about IE8. They should count themselves lucky. Unfortunately for an older site, IE8 was the big kid on the block, so it had to be supported. And now dropping IE8 support has a tangible cost because it most likely will mean some amount of lost revenue over time.

By the time I  left Zazzle, we were still "officially" supporting Internet Explorer 8 although it progressively received less and less love. There were continuous discussions about officially not supporting IE8, but it never got traction because IE8 still accounted for a (small) percentage of revenue.

When I arrived at Eventbrite in May, they had only just recently deprecated Internet Explorer 7! So a lot of my motivation for this blog post is to convince my team that we should go ahead and deprecate IE8 as well. There are just too many reasons why it should be dropped. And hopefully by also sharing these reasons publicly, we can quickly enter a world where IE8 is just a figment of our collective imaginations across the developer community.

At Eventbrite we have a much more formalized process around browser support and deprecation that I really like. Essentially we having a grading system for all browsers:

  • "A grade" - These browsers are supported sitewide. All flows should function according to design and there should be minimal to zero deviations in visual parity to the design. Progressive enhancement for CSS3 features is allowed. These browsers are actively tested.
  • "B grade" - All flows should function properly across the site and all content should be accessible. However, only “key” flows need to have visual design parity. These browsers are only occasionally tested.
  • "C grade" - Content on “key” flows should be accessible and the page shouldn’t look overtly broken. Design bugs in other parts of the site most likely will not be fixed. Only the “key” flows are tested.
  • "Unsupported" - If the page works, great! But don’t be surprised if there are many bugs. They won’t get fixed. They probably won’t be found because there’s no testing anyway.
Then there is a formal proposal process to downgrade a browser. Right now IE8 is a "B grade" browser with talks to downgrade to "C grade." I would like to kick it all the way to "Unsupported" never to be seen again!

Unfortunately, you can’t just say "hey, let’s stop supporting IE8!" and it happen immediately. If your site has been supporting IE8 for years there will be real revenue lost by no longer supporting it. So in order to convince folks, you need to come armed with data and facts to present a compelling argument, especially for ecommerce sites where the revenue loss can be calculated directly.

Arguments for IE8


Before we bury Internet Explorer 8 under a mountain of evidence, let’s give it a chance to mount its own defense. It can explain why we’re at a place where we’re still supporting a browser in 2015 that was built in 2009 and why it’s not so easy to get rid of it.

Exhibit 1: Windows XP needs IE8


Microsoft Windows XP logo


Essentially what it boils down to is that Windows XP users cannot upgrade their Internet Explorer browser past IE8. Internet Explorer 9 won’t work on Windows XP. Furthermore, newer versions of Windows have either been terrible (like Windows Vista & Windows 8) or have much higher system requirements (like the new Windows 10). This means that XP users have trouble upgrading to an operating system that supports a higher version of Internet Explorer. Their only option is to get a newer computer, which is costly whether they are an individual or part of a company.

And speaking of companies, most IT departments at big organizations prevent employees from installing applications or new browsers. It’s a "security risk." So even if a work employee wanted to switch to the browser they use at home, they wouldn’t be able to. Therefore they have to wait until their IT department first switches all computers onto a newer version of Windows. This is how we’re in a world where we have millions of people on a browser from 2009 running on an OS from 2001.

Exhibit 2: People are still using IE8


About 2% of Internet traffic worldwide comes from Internet Explorer 8. While that may not seem like much, what company wants to throw away 2% of revenues? In a world where an experiment (A/B test) is a success if it results in a conversion lift of just 2% and a failure if there’s any drop off in conversions, potentially losing 2% of traffic is a tough pill to swallow. Furthermore small percentage points of revenue can still be millions and millions of dollars being left on the table.

Arguments against IE8


The reasons for supporting Internet Explorer 8 are pretty significant, especially when you factor in the likely loss in revenue. But let’s take a look at the (many) reasons why we should no longer be supporting IE8. Once you’ve heard them all, you too will be convinced that they overcome the reasons in support of IE8.

Exhibit 1: IE8 is over 6 years old(!!!)


Internet Explorer officially debuted on March 19, 2009 and was actually widely praised. It initially started off feature-rich, but because it was never updated to add new features being added to other browsers, it quickly became outdated. Browsers age in dog years. Microsoft deserves the blame for choosing to only have major releases spread apart over years instead of continuous micro updates like the evergreen browsers of today.

In fact, IE8 doesn’t even have an auto-upgrade feature. IE8 users have to manually update their browser to IE9. It feels as if the developer community is always dragging along IE browsers because they have sizeable market share, yet update so slowly. Thankfully this should be alleviated with the new Microsoft Edge browser.

Just for kicks, let’s look at what other browser were around back in early 2009 when IE8 was released:
  • Chrome - Version 2 (now 44)
  • Firefox - Version 3 (now 39)
  • Safari - Version 4 (now 8)
  • Opera - Version 9 (now 31)

Exhibit 2: Other (newer) browsers are available for WinXP


Now the defense mentioned that work employees are unable to upgrade their IE8 browser because of restrictions. That’s true, but not for everyone. Some people are just lazy. Others are ignorant to the fact that "Internet Explorer" and the "Internet" are not the same thing. The latest versions of Chrome, Firefox and Opera all run on Windows XP. They could switch! By the way, Apple stopped supporting Safari on Windows in 2009 so that’s why that’s not a viable option.

So for those users who are able to switch, but are just unaware that they can do so, sites can add a banner alerting those users to the fact that their browser is out of date and that they need to upgrade or switch to a different modern browser. The banner can even link to Browse Happy to list out their browser options.

Exhibit 3: IE8 has minimal traffic


Global browser statistics


The defense mentioned that IE8 makes up about 2% of traffic. Well, that’s a really tiny percentage! And in fact that actual number is 1.91% of worldwide traffic across all devices and is steadily dropping monthly according to StatCounter. It is the 8th most trafficked browser when you group the various Chrome and Firefox evergreen versions together. Other notable stats:
  • IE8 accounts for 2.55% of North American traffic and 2.72% in the U.S.
  • Europe’s IE8 traffic is 1.31% and Asia’s is 1.84%.
  • China may very well be the largest user base of IE8 at 5.45%
  • When just looking at desktop traffic, IE8 comprises 3.18%. It’s 4th after Chrome, Firefox & IE11
  • Not surprisingly Windows XP still makes up 6.21% of traffic
StatCounter provides a code snippet that web developers can put on their websites to track their traffic. It’s been around since before Google Analytics. Because they’re used on such a vast array of websites, they may be the best authority on global traffic numbers. Besides Google of course. But we’ll talk about Google in more detail in a little bit.

Of course what’s most important is a particular site’s own traffic distribution across browsers. It doesn’t matter if global IE8 traffic is less than 2% if a specific site’s traffic is 50% IE8. However, it’s more likely that a given site’s numbers are less than the worldwide numbers. As sites pay less and less attention to IE8 (even if it is "officially" supported), the site’s functionality progressively degrades in IE8, which in turn decreases its IE8 traffic. This is the case currently with Eventbrite. Our numbers are actually significantly less than the global numbers.

Exhibit 4: Traffic % and revenue % are not the same


Most sites track the success of their site by its conversion rate. A simplified explanation is that it is the number of people who successfully complete some important task divided by the total number of visitors to the site. That important task could be a purchase, click on an ad, share/like/post, etc.

Sites also have the concept of average order size (AOS). This is mostly used for ecommerce sites, but could still apply broadly. Essentially it answers the question: when a person purchases, clicks or shares, how much of it do they do? A person who spends $100 on a site is more valuable than someone who only spends $10 with each order.

It’s well-known that browsers on Apple devices tend to generate more revenue on ecommerce sites. This isn’t because Apple devices are better, but because of the demographic of users who own Apple devices. They, on average, have more money to spend. That’s how they have Apple devices in the first place! Now think about the demographic of users still stuck on IE8 and XP. Would you classify them as big spenders or otherwise highly active? Probably not.

Of course each individual site will have to look at their own data to see how things shake out. But contrary to what the defense would lead you to believe, a 2% loss of traffic from dropping IE8 probably will not mean a 2% loss in revenue. It will likely be much lower.

Exhibit 5: Other big companies have dropped IE8


In 2011, Google announced that their Google Apps services would "only support the latest version of Google Chrome as well as the current and prior major release of Firefox, Safari and Internet Explorer on a rolling basis." Each time a new version of one of those browsers is released, they begin to "support the newly released version and stop supporting the third oldest version." So in keeping with their plan, Google officially dropped support for Internet Explorer 8 on November 15, 2012 on the heels of the IE10 release. After that date, users accessing Google Apps service via IE8 got a message recommending that they upgrade their browser (see Exhibit 2).

Why did Google adopt this policy? Here’s their official statement:

At Google, we’re committed to developing web applications that go beyond the limits of traditional software. Our engineering teams make use of new capabilities only available in modern, up-to-date browsers which also provide improved security and performance.

At the end of 2013, Google announced that Google Analytics would stop supporting IE8. Google Analytics would of course still measure IE8 traffic, you just would no longer be able to reliably read your reports in IE8.

Other notable companies giving IE8 the boot:
But none of those companies can compare to Microsoft deciding to distance itself from IE8. It's very own browser! On April 18, 2014, Microsoft ended support for Windows XP, begging users to upgrade to a modern operating system like Windows 8.1. This also unofficially ended support for IE8 as well; at least for IE8 running on WinXP. But Microsoft will officially stop supporting Internet Explorer 8 on January 12, 2016. At that point, there’s absolutely no reason for websites to continue to hang on. But we don’t have to wait until 2016! Drop it now!

Exhibit 6: Windows XP has security issues


Malicious hackers continue to exploit operating systems. Windows XP is one of their favorite playgrounds. And now that Microsoft has ended support for WinXP, any exploits found won’t even be patched! Shouldn’t this be enough motivation for IT departments still stuck on XP to final move to a new OS like Windows 10?

Microsoft on its official "stop using Windows XP" page listed the following potential risks of continuing to run Windows XP SP3 after support stopped on April 8, 2014:
  • Security. Without critical Windows XP security updates, your PC may become vulnerable to harmful viruses, spyware, and other malicious software which can steal or damage your business data and information. Anti-virus software will also not be able to fully protect you once Windows XP itself is unsupported.
  • Compliance. Businesses that are governed by regulatory obligations such as HIPAA may find that they are no longer able to satisfy compliance requirements. More information on HHS’s view on the security requirements for information systems that contain electronic protected health information (e-PHI) can be found here (HHS HIPAA FAQ - Security Rule).
  • Lack of Independent Software Vendor (ISV) support. Many software vendors will no longer support their products running on Windows XP as they are unable to receive Windows XP updates. For example, the new Office takes advantage of the modern Windows and will not run on Windows XP.
  • Hardware manufacturer support. Most PC hardware manufacturers will stop supporting Windows XP on existing and new hardware. This will also mean that drivers required to run Windows XP on new hardware may not be available.

Exhibit 7: Windows XP and https don’t mix (well)


HTTPS Everywhere logo


At Google I/O 2014, Google called for "HTTPS everywhere" on the web. Every site should provide extra security by running their entire site on https. Gone are the days when log-in and checkout were the only portions of the site in https. Google is also starting to use https as a ranking signal.

SHA-2 is now the standard for providing https encryption on the web. However, any SHA-2 encrypted website being viewed by IE8 running on XP SP1 or XP SP2 simply won’t work. It only works on Windows XP, Service Pack 3. And only because Windows added it to SP3 after the fact. Apparently Eventbrite ran into this very problem shortly before I started. As a result, Internet Explorer 8 on SP1 or SP2 was officially moved to "Unsupported."

Exhibit 8: IE8 has no ECMAScript 5 support


As we learned in the History of ECMAScript blog post, ECMAScript 5 was released in December 2009, nine months after Internet Explorer 8. Therefore, IE8 doesn’t have any ES5 support. Quick overview on ES5 features lacking in IE8:
  • Strict mode
  • Array methods like indexOf, forEach, map & filter
  • Object methods like keys & create
  • String methods like trim
  • Function.prototype.bind
  • Reserved words as keys for object literals
  • Allowance for dangling commas (can be fixed by JS minifiers)
  • JSON parse & stringify (technically works but IE8 has to be in standards mode)
We also learned in the Using ES6 right now blog post that the best way to use ES6 is to transpile it down to ES5. But IE8 doesn’t support ES5 So there’s a good chance that our transpiled code won’t be able to run successfully in IE8. Stop holding us back IE8!

Exhibit 9: IE8 has no HTML5 support


HTML5 logo


The fifth revision of the HTML standard (aka HTML5) was officially completed and released in October 2014. However a lot of its functionality had made its way to browsers long before. IE8 does not support any HTML5. Neither does IE9. IE10 only supports a little bit.

HTML5 introduced a number of new elements, new attributes on existing elements, and new native JavaScript APIs. According to caniuse.com, there are 42 different HTML5 features none of which IE8 supports. Here are some of the most popular ones:
A lot of these HTML5 features do come with JavaScript shims via external libraries, but those introduce more problems we’ll get to in Exhibits 14 & 15.

Exhibit 10: IE8 has no CSS3 support


CSS3 logo


Coupled with HTML5 was CSS3, which greatly expanded the expressiveness and capabilities of CSS. Many things that developers had to resort to coding in JavaScript could now be done more optimally in CSS. Here’s a tasty list:

Exhibit 11: IE8 has no ECMAScript 6 support


ES6 logo


Naturally if IE8 doesn’t support ES5, it’s definitely not going to support ES6. You can find the full features list in the Goals & Features of ECMAScript 6 blog post. And remember, since IE8 doesn't support ES5 there's a possibility that your transpiled ES6 code will not work in IE8 either. Way to hold the Web back, IE8...

Exhibit 12: Graceful degradation doesn’t work


Graceful degradation was a big promise in 2013 and 2014 during the big HTML5 hoopla. Web developers could start using new HTML5 and CSS3 features immediately because the old browsers that didn’t support them will gracefully ignore the functionality that it doesn’t understand. While this was technically true, in practice it really was not.

Graceful degradation worked well for what can be described as "icing" features. These features like input placeholders, rounded corners, and transitions. It didn’t work so well for "cake" features like layout. Flexbox made layout ridiculously easy with CSS3, but the "graceful degradation" result is a broken layout in IE8. So in order to not have a broken experience in a supported browser, we developers are left with float or inline-block hacks. Ugh.

Exhibit 13: Mobile first fails


Mobile-first logo


Mobile marketshare increases every month as more and more users access their favorite websites on their mobile devices. While StatCounter says about 40% of traffic is non-desktop, many sites are seeing more than half of their traffic come from mobile. This is why Luke Wroblewski pushed for mobile-first design way back in 2009, publishing his Mobile First book in 2011.

Even if we get mobile-first designs from designers, we still cannot implement mobile-first because of IE8. Ideally we would write our CSS such that the default styling is for narrow (mobile) screens. Then using media queries, we can write CSS for devices larger than narrow screens such as tablets. We could continue the process targeting large desktop screens and even huge TV screens.

But because IE8 does not support media queries (see Exhibit 10), we’re left with two sucky options. We write our CSS such that the default styling is for large screens and work our way down. This is effectively mobile-last implementation and usually results in overriding a lot of CSS rules to remove stylings not needed at smaller screen sizes. Our other option is to do a mobile-first implementation and have separate IE8-specific CSS. This results in a large amount of duplicate code between it and the CSS targeting the desktop screen sizes.

Exhibit 14: IE8 forces code bloat on modern browsers


I’ve already alluded to it in Exhibits 8, 9 & 13, but it’s worth focusing in on the problem of code bloat. Because IE8 doesn’t support so many features available in today’s modern browsers, many people have come up with workarounds:
  • Trick IE8 into thinking it has modern functionality via shims/polyfills (like eventie, classie & doc-ready)
  • Leverage libraries that provide the missing functionality
  • Detect via Modernizr that IE8 doesn’t have functionality so we can use fallback code (like Flash for HTML5 video)
All of these approaches result in additional code that would not be needed if IE8 had native support for the functionality. Modern browsers get all of this just-for-IE8 code as well even though they have native support. Modern browsers are heavier because of IE8.

Think about libraries like jQuery and underscore.js that are used so widely that they’re basically standard. But when you look at underscore.js’s API, a lot of its methods are just wrappers over what’s now become native functionality introduced with ES5 and ES6. But the library still needs to implement the functionality itself in JavaScript to support the browsers like IE8 that don’t have the native support. How much lighter would underscore.js be if it could rely on the native JavaScript support?

And what about jQuery? They introduced jQuery 2.0 back in 2013 and were able to shave off 12% of code simply by no longer supporting IE 6/7/8. This was a step in the right direction; moving the web forward. jQuery’s developers could stop worrying about IE8 and focus on evergreen browsers. However, jQuery announced jQuery 3.0 that is supporting two parallel codebases. The preferred jQuery 3.0 targets evergreen browsers and is the successor to jQuery 2. But there is also jQuery Compat 3.0, which succeeds original jQuery and continues to support legacy browsers. So much for the jQuery team not having to waste their time with IE8...

Exhibit 15: IE8 runs even slower emulating modern browsers


What happens when your site needs to display videos and in order to support IE8 you use a library that falls back "gracefully" from HTML5 video to Flash? What happens when an animation is critical to some site functionality so you have to resort to using jQuery instead of making use of CSS3 transitions? What happens when your site relies on geolocation and in order to support IE8 you need to include a large library that uses IP address location when the geolocation API is unavailable?

What happens when you try to get IE8 to support functionality that it was never intended to support? You throw a lot more code at it and it runs even slower! Think about all the extra CSS hacks needed to try to emulate CSS3 flexbox for IE8. Think about all the jQuery written by developers to simulate media queries, calc(), and HTML5 placeholders. Not only are we creating a bloated code base for modern browsers (revisit Exhibit 14), but we’re also slowing down the very browser we’re trying to support. It may be functional, but it’s barely usable.

Exhibit 16: UI frameworks aren’t supporting IE8


The proliferation of UI frameworks is what has enabled developers to keep building more complex websites. Everyone doesn’t have to reinvent their own wheel every time they build an app. One person (or a group of people) can invent a wheel and share it with thousands of people on Github.

The UI framework developers are also getting tired of having to support IE8. We already discussed jQuery, what they did with jQuery 2 and what they wish they could do with jQuery 3. But it’s not just jQuery. Other frameworks are dropping IE8 as well:
All of them cite wanting to "move forward" and "not be slowed down" by having to support such a legacy browser.

Exhibit 17: Testing IE8 is a pain


Ideally if you officially support a browser, you should test features on that browser before shipping it to your users. That’s not so easy with older versions of Internet Explorer, including IE8. How does one get their hands on a 6-year-old browser? A Microsoft-supplied virtual machine of course! We at least have to applaud Microsoft for providing a way for developers, particularly those not on Windows, to test IE browsers.

But even if you are able to get a multi-gigabyte VM running or even if you are able to use IE11 and change the document mode down to IE8, you’re still testing on IE8! It’s just another browser to have to test. As a result, it doesn’t get tested in development. And unless it’s a big feature, it probably won’t get tested during QA either. More than likely it’ll be users calling into Customer Support who will first notice the IE8-specific issue.

Exhibit 18: Debugging visual issues in IE8 is a pain


IE7 tax


As a developer, which would you rather be doing? Debugging why a <div> that should have some height is collapsed in IE8, or moving on to the next new feature? Yeah, I thought so. In my opinion, the debugging tools in IE browsers are subpar compared to other browsers. And this is particularly true for IE8 and debugging CSS.

The cost in having to fix bugs in a legacy browser is real. So real that a company called Kogan in Australia implemented the world’s first Internet Explorer 7 Tax back in 2012 for IE7 users. At the time, they added a 6.8% surcharge on all purchases "by anyone still insistent on using the antique browser."

Exhibit 19: IE has a bad rep


Break up with Internet Explorer 8


All Internet Explorer browsers now have a bad reputation for being terrible browsers. IE11 is actually a pretty good browser. It is as standards compliant as the other modern browsers, but because of IE8 (and its predecessors) it still has the "IE stench." That "stench" is so strong that Microsoft is completely forgoing the Internet Explorer name and rebranding its new browser as Microsoft Edge. Edge will be an evergreen browser so it’ll always be up to date with the latest features.

But we’re still stuck with IE8, much to the disdain of web developers worldwide. So in order to publicly share their disgust, Break Up with IE8 was born. There you can share your IE8 break-up letter with the public.

Exhibit 20: Once IE8 goes, so does IE9 & IE10!


Let’s end on a positive note. If we look back at StatCounter’s global traffic numbers, IE8 actually had more traffic than IE9! And IE9 had more than IE10. The vast majority of IE9 and IE10 users can upgrade to IE11 and have already done so. The only real reason to support IE9 & IE10 right now is because we’re still supporting IE8. But after these compelling arguments, IE8 is all but out the door. And it might as well take IE9 and IE10 with it.

This means that if we drop IE8 (and IE9 & IE10 with it), we only need to support IE11 and Edge. We’ll now all be on the Google support policy. And with Microsoft giving away free upgrades to Windows 10, soon most people will be on Microsoft Edge as well. If Microsoft gets its way and everyone quickly gets on Edge, we only need to actively support 5 browsers: Chrome, Firefox, Safari, Opera, and Edge. What a wonderful world that would be!

Closing Arguments


If you’ve actually read this far, congratulations! You’ve definitely been inundated with more evidence than you thought you ever needed. It’s pretty much an open and shut case, right? Now what’s left is for the judge (aka the boss) to rule in favor of dropping IE8. All I ask is that you celebrate the verdict in style. It’s certainly well-earned.

Now my personal objective is to take this evidence to my team at Eventbrite and try to get IE8 kicked completely off the supported list. Let’s just skip the whole "C browser" stage altogether. We can make IE9 the "C browser" and IE10 the "B browser," even though I’d prefer that they all get kicked off of the list. Wish me luck! I’ll report back with updates.


Feedback? Additions? Differing opinion? Feel free to leave a comment or tweet me @benmvp.