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.


Comments
gravatar James Kubecki Thursday, September 1, 2016
"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." It seems that you're saying that it doesn't return the value, unless you explicitly return one, or enclose in parentheses. But that's just the way the syntax works... https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#Returning_object_literals (Would include the actual ECMA spec on it, but hey, who can read those things?)
gravatar Jamie Thursday, September 1, 2016
It also is worth mentioning that the browser support for arrow functions is not great https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions#Browser_compatibility I ran into this problem where it doesn't work on mobile devices (Safari)
Alex Friday, September 2, 2016
I've always found the "this" keyword in JavaScript to cause more confusion than is worth the convenience. As many wise people have said, you read code more than you write it, and when reading code I'd rather not have the headache of trying to figure out the nuances of "this"! Great point about the object literals - that's presumably a pretty common use case. I wonder if there's a nicer way of achieving the same thing without the parentheses?
gravatar Sam Friday, September 2, 2016
Interesting, I didn't know of all these dragons... I might add that arrow functions are always anonymous, which can make it painful to debug. Regarding the `this` keyword I tend to use it very sporadically anyway, when I really have to.
gravatar Max Friday, September 2, 2016
@Sam: Arrow Functions pick up names the same way that other functions do. It's largely dependent on what browser/dev tools you are using, but the general rule is that arrow functions pick up the name of the variable they are first assigned to. So for example `let add = (numbers) => { /* ... */ }` will pick up the name `add` and similiarly with the `const adder = { add: (numbers) => { /* ... */ } }` example. If you have a bunch of arrow functions to debug sometimes you can just "lift" them into the surrounding scope and assign them to a local variable instead. Additionally, some of the dev tools for some of the browsers are getting smarter about assigning names to arrow functions used as arguments to other function calls and assigning the arrow function the name of the argument it is filling.
gravatar Andy Vennells Monday, September 5, 2016
Can't this issue be fixed by saving the scope of "this" in a variable (var _this = this) and then referencing the scope within the arrow function?
gravatar authenticated user Thursday, September 8, 2016
Hello, I am wondering how you authenticate a user to post a comment here. Sorry, just testing.
gravatar Barry Tuesday, September 13, 2016
When NOT to use an arrow function; When you really need 'this'. When you need a method to bind to an object. When you need to add a prototype method. When you need arguments object. If you stick to the above you'll be fine.
gravatar jose Friday, September 16, 2016
Thank you for dedicating time to write these posts, they're always informative. I gave up on "this" for good. It has given me a lot of headache and not one moment of joy.
Comments are closed.

My Pluralsight Courses

K.Scott Allen OdeToCode by K. Scott Allen
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!