this JS ain't right - Arrow functions vs Function expressions

Hank Hill confused about a JavaScript error

“Cannot read properties of undefined”

Have you ever been stuck on one of these errors, especially when dealing with JavaScript’s this operator? Your code seems to be right, you even verified that it works in another place, but for some reason it is failing now. You’ve scoured through documentation to find clues about what could be going on, but you haven’t found anything worthwhile. You’re unsure if you’re even asking the right questions.

While I’m not going to solve all your “Cannot read properties of undefined” issues with this one article, I think it will be instructive to study one such scenario where a misunderstanding of how this works could have easily ended up frustrating you. Hopefully, by the end of the article you’ll come away with both an increased understanding of this, and an increased confidence that any errors you run into are just another learning opportunity, given enough patience.

Consider this piece of code in a Stimulus controller’s connect method:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["cell"];

  connect() {
    document.onkeydown = function (e) {
      if (e.code === "Backspace") {
        let selectedCell = this.cellTargets.find((cell) =>
          cell.classList.contains("selected")
        );
        selectedCell.innerText = "";
      }
    };
  }
}

Without getting into why the controller is written in this way, let’s try to figure what the author of this code intended to do.

If you’re familiar with Stimulus and Javascript, that should be pretty straightforward to answer. There is probably some HTML element somewhere which is wired to this controller. When the HTML element appears on the page, Stimulus detects that and calls the controller’s connect method. When connect is called, it sets document.onkeydown to a function. When the user hits a key on their keyboard, document.onkeydown will be called and if the key was a “Backspace”, this function will look for a cellTarget which contains the class selected. Once it finds this element, it will blank out its innerText.

Are there any issues with the code?

The first one that comes immediately to mind is that find may not actually find anything, and when you try to set innerText on a null or undefined object, the JS interpreter will throw an error. That’s easy enough to fix though.

The second issue, which is a bit more sneaky, is that you’ll run into this error when the function runs:

Uncaught TypeError: Cannot read properties of undefined (reading 'find')

The JS interpreter is essentially saying that you can’t do undefined.find(...), which means it thinks this.cellTargets is undefined. What’s going on here, and how do we fix this?

The first place to look would be the static targets = ... definition at the top. Do the name we’ve given our target and the method we’re using to access it match up? It seems like they do, so that can’t be it. To figure out why this.cellTargets is undefined, we need to figure out what this is. In a typical Stimulus controller, this usually refers to the controller instance. However, in this case, there’s a curveball being thrown our way.

In JavaScript, this is special. Its value inside a function depends on: how the function is defined, and how the function is called.

this depends on how a function is defined

There are two common ways of defining functions in the JavaScript I’ve been exposed to and write on a regular basis. “Function expressions”, where we use the function keyword to define a function. Or “Arrow function expressions”, where the function keyword is omitted and the arrow symbol => is used to declare a function.

When this is used inside a function expression, its value depends on how the function is called. In particular, the function could either be called by itself, without being accessed on anything, or it could be called as a property of some object.

// defining a function with a "Function expression"
function getThis() {
  return this;
}

const myObject = { name: "my object", getThis };

getThis(); // calling the function by itself

myObject.getThis(); // accessing and calling the function through myObject

this depends on how a function is called

When a function expression is called by itself, this will either be undefined (if using strict mode) or be evaluated to what is known as globalThis. In a browser, this is usually the window object.

When a function expression is called through an object, this is evaluated to that object. It doesn’t matter where the function has been defined (inside another object for instance).

const object1 = { name: "obj1", getThis };
const object2 = { name: "obj2", getThis };

// both these statements are true
object1.getThis() === object1;
object2.getThis() === object2;

For methods inside a class, this is always evaluated to the instance of the class. That is what we rely on in Stimulus controllers.

Arrow functions are different

Arrow functions, on the other hand, are a bit simpler. They only have one this value. this, in an arrow function, always evaluates to what this was when the function was defined (also known as lexical scope). Let’s consider a few examples to understand better what this means.

const getThis = () => this;

getThis(); // returns the `window` object in a browser

const myObject = { name: "my object", getThis };

myObject.getThis(); // also returns `window`

To figure out the value of this inside an arrow function, ask yourself this question: If I inserted an empty line right above where the function is defined, what would the value of this on that line be?

In the example above, the arrow function getThis is defined at the top level. In a browser, the value of this is the window object at the top level, and the function therefore returns window. In this case, it doesn’t matter if the function is called by itself, or if it is accessed through an object. If you want to verify this, you can quickly fire up a dev console in your browser and try this out!

class ThisGetter {
  getThisArrow = () => this;

  getThisFunction() {
    return this;
  }
}

const thisGetter = new ThisGetter();

thisGetter.getThisArrow() === thisGetter.getThisFunction(); // returns true

In the scenario above, both functions, when called through an instantiated object, evaluate this to that object.

Back to the issue at hand

In our original example code, we had this:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["cell"];

  connect() {
    document.onkeydown = function (e) {
      if (e.code === "Backspace") {
        let selectedCell = this.cellTargets.find((cell) =>
          cell.classList.contains("selected")
        );
        selectedCell.innerText = "";
      }
    };
  }
}

We know now that the value of this inside the anonymous function expression that document.onkeydown is set to will depend on how that function is called. If it is called like this:

document.onkeydown();

Then this will evaluate to document, and not the Stimulus controller instance. Now, I don’t know what JS does under the hood to actually trigger the event handler, but this is easy enough to verify.

Paste the following into your dev console, and then hit a few keys to trigger the onkeydown event. You should see the document object getting console logged.

document.onkeydown = function () {
  console.log(this);
};

However, if we used an arrow function instead:

static targets = ["cell"]

 connect() {
    document.onkeydown = (e) => {
      if (e.code === 'Backspace') {
        let selectedCell = this.cellTargets.find(cell => cell.classList.contains("selected"))
        selectedCell.innerText = "";
      }
    };
  }

Then we can be reasonably certain that this will evaluate to the Stimulus controller instance, since the value of this where the anonymous arrow function is defined (known as “lexical scope”), is the instance of the class.

Conclusion

We’ve investigated some curious behavior in JavaScript and hopefully come away with an understanding of how the value of this can sometimes be dynamic in nature. If you’re interested in learning more, I highly recommend you check out the reference to the this operator over at MDN web docs.

Equally importantly, the next time you’re stuck on a hard-to-explain error where JS is complaining that it “Cannot read properties of undefined”, you’ve hopefully absorbed some techniques and strategies you can use to reason your way out of the situation.

If you liked this article and want to hear more from me, please subscribe to my email newsletter. I’ll notify you whenever I publish a new article.

Want to be notified when I publish a new article?