To hell with nuance, arrow function all the things

Cory
11 min readJan 6, 2020

Let’s cut to the chase. There have been several high profile folks in the JS community that have laid out some pretty strict limitations on when they think arrow functions are appropriate. Many words have been spent talking about readability, variability, etc. For what little it is actually worth, I’m here to add to the din with a reasoned defense of straight up replacing the function keyword with the arrow function (=>).

I’m normally a pretty nuanced person. There is usually no “right” way to do something. Just various ways with different trade offs. Really, that’s all I want to suggest here. A way to write functions in JavaScript — among many — that has benefits and drawbacks. It’s just, in my own experience doing this exclusively for the last four years, I have yet to find a situation where the drawbacks have even approached the benefits. This is about as close to an absolute as I get.

JavaScript is at it’s best when teams define the slice of it that they are going to work in. A consistent way of doing something within a code base is often of more importance than the “right” way. Consistency can be reasoned about, compared, and debated. But perhaps most importantly, consistent patterns don’t require debate or conversation. They can become habitual and reflexive. Mental energy can be applied to other, more pressing issues.

So without further ado, here is why I haven’t used the function keyword in over four years, and I don’t intend to.

this

The this reference in JavaScript is shared mutable state who’s scope (and reference) is the object itself and is implicitly available within either the object’s constructor function, or the class definition depending on what one is using. It’s not just mutable state, it’s a mutable contextual reference that may itself contain state, methods, properties, prototypes, etc. It is worse than a leaky abstraction as it leaks virtually everything by default, and allows mutations of what it leaks. It is the problems with this that is the primary motivation around the animus many have toward JavaScript classes.

Hands down, the single biggest win with arrow functions is a complete lack of a reference to this within it. In my experience, patterns that rely on this, and misunderstandings about how this works are the greatest source of bugs in a system. There is nothing that this offers that cannot be accomplished without it — most of the time with less complexity. If this can be avoided, it should be. Occasions where this cannot be avoided are almost always because an API that one needs to integrate with made the decision to leverage patterns that rely on this. Arrow functions don’t give you the option, so win in my book.

New! no new

Arrow functions cannot be used as constructors. One cannot cause mischief and confusion by using new (or failing to) on an arrow function like one can with functions defined with the function keyword. With traditional functions, the behavior of a function changes depending on if it is invoked with new or not. This is enough of a problem that prior to proper classes, patterns arose to ensure that constructors were called properly with new

function Named (name) {
if (!(this instanceof Named)) return new Named(name)
this.name = name
}

Today, if one wants a class, one uses a class. There really is no reason to use a function as a constructor. Similarly, I’m not finding much of a reason to use traditional functions as… well… functions. Arrow functions provide all the tools I need.

Arguments

Less problematic, but related to the first two points, is the absence of the arguments reference when using arrow functions. Unlike functions defined with the function keyword, an arrow function does not have access to the implicit arguments reference. Instead, if an arrow function is meant to accept an arbitrary number of arguments, the rest operator () is used to gather up all remaining arguments not specified in the signature.

// old and bustedfunction sum () {
const tail = arguments.slice(1)
const head = arguments[0]
tail.reduce(function (sum, num) {
return sum + num
})
}
// new hotness
const sum = (head, ...tail) => tail.reduce((sum, num) => sum + num, head)

This has a number of advantages. Not the least of which is that we are no longer dealing with an implicitly available reference within the function. If we need it, we need to declare it, right in the signature.

Another benefit that is demonstrated in the example above is that with the rest operator, we get a proper array that we can operate on directly. The arguments reference we when using the function keyword gets something that kind of looks like an array, but has none of the methods. It’s array-like, but it’s not an array. In the example above, we need to coerce the arguments object to an array with slice in order to begin working on it the way we want to, because an arguments object does not have a map method.

Readability

There doesn’t seem to be much data around the readability of arrow functions vs traditional functions, so I can only rely on my own experiences, but I teach and interact with a lot of new developers and what I hear from them backs this up. Most people don’t have issues with the readability of arrow functions. For those that do, it often appears to be one of familiarity. In my experience, newer developers who are taught arrow functions first tend to find functions defined with the function keyword slightly more difficult to read. As with so many things, one’s own experiences and preferences play a huge role in whether arrow functions are seen as more readable or not, but just as universal, the more exposure one has, the more familiar a thing becomes. Arrow functions are no different. If you find them harder to read than the function keyword, using them more often will likely alleviate that.

Another facet of readability is the signal to noise ratio of the syntax. Let’s look at a very simple example:

function add2(arg1) {
return arg1 + 2
}

The example above takes a single argument, and adds 2 to it. There are a few things with the syntax in this example that give meaning to the reader. The function keyword informs us that we are defining a function¹ (obvi). add2 is how we reference the function. The ( and ) let us know that what comes between them are arguments. The { and } provide bounds for the function body; and finally the return statement lets us know what calling the function will (or may since we can have multiple return statements) resolve to when called.

Character count is not a perfect proxy for syntactic complexity, but it’s sufficient for our needs. The function declaration form of our example took 42 characters to express. We will use that as our baseline.

Now, let us compare what this could look like with as an arrow function:

const add2 = (arg1) => arg1 + 2

Let’s see how our visual cues compare to the more traditional function declaration. Initially we have const which tells us we are declaring a reference. Immediately after that we have our reference identifier. This is how we reference what ever it is. Next is the = which tells us we are about to see what value is being assigned. So far, this is exactly like any other assignment we might see. This pattern; (const|let|var) [identifier] = is perhaps the most common pattern our brain sees when reading JavaScript and is grokked as if it were all one “word”².

Immediately following that, we see ( . We are now able to infer that we are assigning a function to our reference³, we are immediately in function mode now. We find the closing paren ) and recognize the end of our argument list. The => tells us the function body comes next. Our brain has long ago recognized that this is all on one line, so we don’t even bother looking for or expecting either curly braces or return statements. We know that what follows will be evaluated and returned.

As you can see, all our visual cues are accounted for with our arrow function form, and it took only 31 characters. That’s a 26% savings on our function declaration form. That’s 26% less visual parsing to get all the same information. Once familiarity with the arrow function form is on par with function declarations, that’s 26% less brain processing too. I’ll take a 26% efficiency improvement any day.

Admittedly, this efficiency improvement does not scale as a function increases in size, but regardless of which function form we prefer, it is generally agreed upon that smaller functions are better than larger ones.

Variability

This, I will concede, is probably arrow function’s biggest drawbacks. There are (too?) many ways to write an arrow function. Under various circumstances, parts of the syntax of a functions may be omitted. For this cause, it’s variability actually gives support to arguments against its readability, and I again concede that certain forms of arrow functions I find have more friction in reading than others.

Verbose functions

The most verbose form of the arrow function syntactically just replaces the function keyword with => and move it to the other side of the arguments list. There’s still quite a bit of syntactic noise.

(arg1, arg2) => {
return {
sum: arg1 + arg2
}
}

Single argument functions

In cases where there is only one argument, it is not necessary to enclose that argument with parentheses. In my view, this eliminates only a tiny bit of “unnecessary” syntax, but I find that the open parentheses provides an initial cue that we are entering “function mode” and is valuable enough to retain. Without it, the first indication we are in function mode is the arrow (=>) which by the time we get to it we have already had too much ambiguity to safely put our minds into function mode.

arg1 => {
return arg1 + 2
}

Single expression function bodies

In cases where the result of a single expression is to be returned, the curly braces that normally bound a function body, as well as the return keyword may be omitted. This right here is the money shot. If we accept the unix philosophy of simple, short, clear, modular, and extensible code, then functions that fit this condition should be our most common form of function. As such, they benefit the most from this shorthand. All syntax that does not provide value is removed and what is left is about as clear and concise as one can get without sacrificing one for the other. Most of my functions fit this pattern.

(arg1) => arg1 + 2

There is one caveat to this form. If the expression is an object definition, then in order for the object definition to not be misidentified by the compiler as a function body that contains syntax errors, the object definition must be wrapped in parentheses.

I’ll be honest, this has slowed down authorship just a bit, as I often find myself having to mentally backtrack and wrap the object in parentheses. With that said, a small hit to authorship is vastly outweighed by the improved readability, because without the parentheses, our brains fall into the same assumptions as the compiler and begin reading the object as a function body. The syntax that informs the complier that the code that comes after the arrow is actually an object, not a function body, also informs our brain of the same thing!

// 🙅‍♀️What even does this mean?
(arg1) => {
sum: arg1 + 2
}
// ️🙆‍♀️O I C. It's an object.
(arg1) => ({
sum: arg1 + 2
})

You may remember this little nugget from earlier;

JavaScript is at it’s best when teams define the slice of it that they are going to work in

-Me, just a few paragraphs ago

Well, it’s particularly fitting for the various incarnations of arrow functions. If I may be so bold as to suggest the style my team and I prefer:

  • Always include argument parentheses, regardless of the number of arguments. This provides an early visual indication that we are dealing with a function, which otherwise wouldn’t come until after the argument. By that time, we have already likely dropped into another context mentally such as variable assignment.
  • Prefer functions that are single expression function bodies and eliminate the syntactic noise of the curly braces enclosing the function body and return keyword. This has the tendency to reduce complexity by keeping functions small and focused, an orthogonal, but important goal nonetheless.

It is not always possible, or reasonable, to have only single expression function bodies. In those cases, we end up with the most verbose form of the arrow function. But even in the most verbose form, we still have reduced cognitive load due to the universal assignment pattern ((const|let|var) [identifier] =) — remember our brains treat that pattern as one “word” and have the benefits of those missing “features” of new, this, and arguments. Regardless of the style used, arrow functions remove unnecessary complexity and cognitive load when used exclusively, without sacrificing any functionality that isn’t replicated by better patterns anyway.

So, to hell with nuance, arrow function all the things!

[1] The function keyword caries with it some other implications that are worth considering too, as they can result in some nasty bugs if incorrect assumptions are had. For instance, in the example the line begins with the function keyword. This tells us it is a function declaration — as opposed to a function expression. A function declaration is subject to function hoisting, meaning it can be referenced at any point within the current scope, even before the function is defined. That can get really weird when that concept is not clear to the reader. A function expression on the other hand begins with an assignment const add2 = function () {} and is not hoisted. It can only be referenced after it is declared. It can get even more confusing if both forms are used at the same time: const add2 = function addTwo() {} . One might expect that addTwo is hoisted and later assigned as a reference to add2 but it is not. Attempts to reference addTwo prior to it being assigned to add2 result in a syntax error 🤦‍♂️. You might see this combined form espoused by some as it gives the name of the function in the stack trace should an error be thrown inside the function. That seems useful in theory, but in practice, I have never not been able to readily know where an error is thrown because the function is listed as anonymous function in the stack trace. The context of the error usually provides sufficient information to track it down in short order.

[2] It’s partly for this reason that even before arrow functions were a thing, I have advocated for not using the function declaration form and instead have preferred the function expression. By leveraging existing, built-in cognitive shortcuts, we cut down on the scope of things that need to be held simultaneously in memory to understand a program as we are reading it. Less cognitive load means fewer bugs, and cleaner code.

[3] Sure, an opening parentheses immediately following an equal sign could technically mean many things and still be syntactically correct, and the compiler can’t know for certain that it’s a function until it gets to => but in practice = ( humans don’t write code that way. Since we are talking about human readability, if someone is defining an assignment that starts with an opening parentheses that isn’t a function they are doing something unusual enough that the readers cadence is going to be broken anyway. Jumping immediately to the assumption that an opening parentheses after an equal sign is a function is exactly the correct assumption to make until/unless further reading disrupts that.

--

--

Cory

Front End Engineer with deep technical expertise in JS, CSS, React, Frontend Architecture, and lots more. I lean functional, but I focus on maintainability.