How to Abuse the Fat Arrow

ES6 is a substantial update to the JavaScript programming language; there are a lot of new things in in there. I am really excited about some of them; block scoping, rest parameters, the spread operator, some of the object initialization shortcuts, templated strings, and default arguments. Heck, I've even come to tolerate class.

Then there are arrow functions (also known as the fat arrow), which introduce the => syntax. It gives you "lexical scoping" which is basically just fancy talk for "the this pointer is what you think it will be."

const Person(first, last) {  
  this.first = first;
  this.last = last;

  setTimeout(() => {
    console.log(`hi ${this.first} ${this.last}`);
  }, 100);
}

const adam = new Person('adam', 'bretz');  

Example 1 - Lexical scope

100 milliseconds after this object is initialized, 'hi adam bretz' is printed to the console. Before arrow functions, you would need to save a copy of this into a local variable, usually called self, and use that in the setTimeout() callback. I'm sure you've either seen this pattern of have written it yourself. There really isn't a good, performant way to get around it. Arrow functions are our way around.


Fat arrows can also be used as just a shortcut for the word function. For example:

const addtwo = function (num) {  
  return num + 2;
}

Example 2 - ES5 function expression

This can be rewritten as:

const addtwo = (num) => {  
  return num + 2;
}

Example 3 - Fat arrow function expression

I personally prefer the style in Example 2 because I'm old and find it easier to read, but I can see the draw of Example 3. It's less typing, and it's fairly obvious by the context that it's a function that takes a single argument and returns that argument plus two.


Things start to go bad when you start being terse for terseness sake. There are two features that can quickly turn your terse code into an unreadable, and difficult to debug mess.

  1. The implied return
  2. Optional ()

When using =>, if you omit {} around the function body, there is an implied return statement. If we refine our example from above, it could be written as

const addtwo = (num) => num + 2;  

Example 4 - Fat arrow implied return

Example 4 is functionally equivalent to both Examples 2 and 3. Now that the return statement is gone, it's becoming a little less clear what addtwo is now. At a quick glance, you might think it's the value of num + 2, whatever num is, and not a function expression.

If we take this one more step and leverage the optional (), Example 4 can be rewritten even more tersely:

const addtwo = num => num + 2;  

Example 5 - Fat arrow missing (), implied return

Example 5 is the Cadillac of short and terse function expression. The temptation of writing code like this is very strong. I mean, look how short that is! It's a single line now! Examples 4 and 5 are extremely basic examples of abusing the features of the fat arrow. In the example below, I'm going to demonstrate how these two "features" can make your code harder to debug for sure and arguably harder to read.


The most common place you'll see fat arrows misused is as arguments to composition or higher order functions. For simplicity, lets work with arrays and some of the higher order functions they have built in.

const values = [1, 2, 3, 4, 5];  
const mutate = (a, b) => Math.floor(a + b * Math.random());

let x =  
values.map((index, value) =>  
  mutate(index, value))
  .reduce((result, value) => result.concat(value), [])
console.log({x});  

Example 6 - Multiple fat arrow functions in array methods

Example 6 is a quick example of what I'm talking about. Suppose you forget the order of arguments for map. You want to log the arguments, just to make sure you've got it right, so you try to add console.log or debugger.

...
let x =  
values.map((index, value) =>  
  console.log(value); 
  mutate(index,value))
  .reduce((result, value) => result.concat(value), []);
...

Example 7 - Trying to debug

Go ahead and try that in the Babel repl. You get a syntax error. How about a debugger statement so you can examine those arguments that way? Nope, that doesn't work either. In order to debug Example 6, you have to make non-trivial code changes. You have to add an extra set of {}, move around some (), as well as explicitly return the result of mutate.

...
let x =  
values.map((index, value) => {  
  console.log(value)
  return mutate(index,value)
})
.reduce((result, value) => result.concat(value), [])
...

Example 8 - Changes needed to debug

So now, you have to make changes just to log information to the console. If you were trying to fix a bug, your workflow would have to be:

  1. Make the change so you can debug.
  2. Make the code change.
  3. Remember to undo what you had to do in number 1, and hope that the change in 1 didn't impact the overall code functionality.

Code needs to be two things; human readable and easy to debug. Example 6 being human readable or not is an open debate. Personally, I don't think it is. However, having to change code just to allow adding a console.log() is an annoying chore. Usually when I start to debug something, logging arguments is one of the first things I do.

Suggested Fat Arrow Rules

Here at Continuation Labs, we have two simple rules that help eliminate these basic problems with debugging and reading code.

  1. Always use () to wrap function arguments. So foo => foo; becomes (foo) => foo;
  2. Never use the implied return. Always explicitly return something. (foo) => foo; becomes (foo) => { return foo; }

As JavaScript developers, we're frequently tempted to express things in as few characters as possible. As far as fat arrows are concerned, the few saved characters aren't worth the mental and debugging overhead.