OdeToCode IC Logo

The Troubles with JavaScript Arrow Functions

Thursday, September 1, 2016

Over the summer I gave a talk titled “The New Dragons of JavaScript”. The idea was to provide, like the cartographers of the Old World, a map of where the dragons and sea serpents live in the new JavaScript feature landscape. These mythological beasts have a tendency to introduce confusion or pain in software development.

Arrow functions have surprised me with the amount of turmoil they’ve created. At first glance, they seem so easy, like when multiplying the numbers in an array by 2.

const numbers = [1, 2, 3];
const result = numbers.map(n => n * 2);
// produces [2,4,6]

But even a simple map operation can run into problems if the code tries to map each element to an object literal using the wrong syntax.

const numbers = [1, 2, 3];
const result = numbers.map(n => { value: n });
// produces [undefined], [undefined], [undefined]

The problem in the above code is that the opening curly brace of the arrow function makes JavaScript think there is a block of code to execute instead of a simple object literal expression to evaluate. The result is an array of undefined. In this scenario the code either needs an explicit return statement, or to parenthesize the object literal.

const result = numbers.map(n => ({ value: n }));
// [{value: 1}, {value:2}, {value:3}]

Ironically, most problems developers encounter with arrow functions center around the problem arrow functions attempt to solve. The slippery JavaScript this pointer. A bit of casual reading about arrow functions will tell you how arrow functions capture the this reference from their lexical environment. With arrows, you can write code like the following without worrying about explicitly capturing this into a local variable.

const adder = {
    sum: 0,
    add(numbers) {
        numbers.forEach(n => {
            this.sum += n;
        });
    }

};

adder.add([1, 2, 3]);
// adder.sum === 6

However, it’s easy to write code that assumes the wrong environment. In the following code we have an arrow function inside an arrow function, so the this reference will not be the adder object, but whatever scope the adder object lives in.

const adder = {
    sum: 0,
    add: (numbers) => { // scope here is important
        numbers.forEach(n => {
            this.sum += n;
        });
    }

};

adder.add([1, 2, 3]);
// adder.sum === 0

The biggest sea dragon on the map in the arrow function waters is highlighted in the “NOTE” section of 14.2.16 of the spec. The takeaway here is that we cannot change the this reference inside of an arrow function. The reference is fixed, it’s baked, it’s  static and permanent. There are implications for two types of code. First is the type of code that expects to manipulate this using bind, call, or apply.

const adder = {
    sum: 0
};

const add = (numbers) => numbers.forEach(n => this.sum += 1);

adder.add = add.bind(adder);

adder.add([1, 2, 3]);
// adder.sum === 0

The second type of code is code that expects someone else to setup this for a call. I first experienced the brain teaser of unexpected this values writing arrow functions with Jasmine. Jasmine sets this to a context object for sharing state between test setups and asserts. Arrow functions, Jasmine contexts and regular functions mix into a broken cocktail. The same problem can arise with DOM event handlers.

it("this is not what you might expect", () => {

    // .. this?

});

Summary

Arrow functions are not a replacement for regular functions in JavaScript. There are situations where arrow functions do not work as expected. I’m not giving up on arrow functions, I still use them when possible. However, I do lament the fact that I still need to fret over every use of this in JavaScript code.