It's been nearly a decade since the 2015 ECMAScript standard (a.k.a. ES2015 or ES6) was released and arrow function expressions were introduced. These arrow functions are a compact syntactical sugar, consisting of implied return statements and concise use of parenthetical characters, yet they simultaneously improve code readability. For comparison, the ES5 version for sorting numbers in an array seems clunky next to the ES6 version:

myArray.sort(function(a, b) {
  return a - b;
});
Javascript code that uses ES5 syntax can take several lines. 
myArray.sort((a, b) => a - b);
Javascript code that uses ES6 syntax is shorter and cleaner.

But despite their many wins, there are also a few gotchas to remember when using arrow functions. In particular:

  1. Arrow functions do not have bindings to this or super.
  2. Arrow functions cannot be used as constructors.
  3. Arrow functions cannot use yield or be generator functions.

That first bullet point has caused many a debugging headache, so I've contrived an example to illustrate how easily bugs are created. Imagine an object with a method that immediately logs a private variable once and then logs the same variable again one second later. A first (buggy) implementation might look like this:

let myObj = {
  myVar: "foo",
  
  myBrokenES5Func: function() {
    console.log(this.myVar);
    setTimeout(function() {
      console.log(this.myVar);
    }, 1000);
  }
}

myObj.myBrokenES5Func();
Of the two log statements, only the first one prints the expected output.

The first print statement works, but unfortunately, the second one doesn't. That's because the two this keywords have different contexts. The first one is bound to the myObj context and can thus access myObj.myVar without issue, but the second this is called inside a setTimeout callback, so it's actually bound to the Timeout context, which doesn't have a Timeout.myVar attribute. That is, the second this has no knowledge of myObj!

An easy fix is to capture the myObj context in a variable called self and pass self to the callback instead of this:

let myObj = {
  myVar: "foo",
  
  myFixedES5Func: function() {
    let self = this;

    console.log(this.myVar);
    setTimeout(function() {
      console.log(self.myVar);
    }, 1000);
  }
}

myObj.myFixedES5Func();
The use of self to capture the myObj context allows both log statements to work properly.

Ta-da! But there's an even better solution. Since arrow functions don't have this bindings, the same fix can be accomplished simply by using ES6 syntax instead of ES5:

let myObj = {
  myVar: "foo",
  
  myCleanES6Func: function() {
    console.log(this.myVar);
    setTimeout(() => console.log(this.myVar), 1000);
  }
}

myObj.myCleanES6Func();
Arrow functions in callbacks remove the need to capture contexts using self.

Okay, this is all fine and dandy. The arrow function fixed the bug and cleaned up the code too, so what's the problem? Well, some developers see how easily using ES6 solved their problem and then decide to convert all their old-school functions into neat arrow functions. Then they end up with broken code like this:

let myObj = {
  myVar: "foo",
  
  myBrokenES6Func: () => {
    console.log(this.myVar);
    setTimeout(() => console.log(this.myVar), 1000);
  }
}

myObj.myBrokenES6Func();
Greedy use of arrow functions can create unintended bugs.

The first this is inside an arrow function, so it's no longer bound to the myObj context. And the second this suffers from the same issue one level deeper. Now neither of the print statements work anymore!