This post written by Jon Klein, co-founder at Verifiablee

Among other languages, I’ve worked with JavaScript professionally for many years. And yet still, more frequently than I’d like to admit, I manage to introduce some scenario that leaves me — at least temporarily — scratching my head, questioning the very nature of my existence (or at least the nature of JavaScript, but anyway). This week, I ran into a couple of interesting ones I thought it would be good to document.

I considered describing these scenarios as “landmines I stepped on” or as “shooting myself in the foot”, but both descriptions seemed too… dramatic for the types of problems I encountered. I settled on “banana peels” because there is a perverse slapstick comedy about these kinds of errors. What’s interesting about these scenarios not just that they happened, but why they happened. Each of the scenarios involves making what I thought was a logical series of changes, only to be hit with some unexpected result. In each of these cases, I slipped — but the language (and in one case the React framework) itself left a banana peel on the floor for me to slip on.

Let me get one thing out of the way upfront — all of these things are dumb mistakes, and they’re pretty much all my fault. Though some languages make it easier than others to fall into traps like these, every language has traps, and I’m a pretty firm believer in “a bad carpenter blames his tools”. I document these issues here not to throw JavaScript (or myself) under the bus, but instead to highlight them in case others run into them. Also, by writing them down, maybe I’ll remember it next time I come across one.

If “value” is defined, “value” is undefined. Of course.

So let me start this one with an incomplete code snippet, as this is what I was looking at in my editor when I ran across the issue:

 const f = arg => {
   if(arg) {
     console.log(arg)

     ...
     ... some other code ...
     ...

  }
}

When this function executed, I got the following error:

console.log(arg)
            ^
ReferenceError: arg is not defined

In conjunction with “if(arg)” check, this seems to be absolutely impossible at face value. I read over the preceding lines over & over again, trying to figure out my error. It wasn’t until I looked later in the file that I realized my error.

const f = (data) => {
  if(data) {
    console.log(data)

    ...
    ... some other code ...
    ...

    let data = ...

  }
}

I had redefined data later on. Yep, dumb mistake on my part, for sure — but what made it a banana peel is the confusing error message, combined with the “distant” effect of the code far below. Instead of the interpreter saying “hey man, you seem to have redefined this variable”, it sort of pretended everything was okay, but resulted in an “impossible” result. Not cool.

In this case, I would have detected the issue using TypeScript — but alas, while part of this project is in TypeScript, I haven’t yet migrated the whole thing over. This particular file was of course, not done yet. Note: just re-reading the above after I wrote it was enough of a push for me to convert the rest of the project to TypeScript, as I should have done earlier!

Iterating with Async Functions

Next up is a gotcha in JavaScript’s async/await keywords which are used to make asynchronous code simpler. Prior to async/await, a JavaScript developer might encounter some form of “callback hell” where a series of asynchronous calls is chained together using callbacks:

getSomeDataAsync((data) => {
  processDataStep1Async(data, (data) => {
    processDataStep2Async(data, (data) => {
      processDataStep3Async(data)
    })
  })
}) 

JavaScript Promises improve on this pattern by allowing asynchronous Promises to be chained together:

getSomeDataAsync(data)
  .then(processDataStep1Async)
  .then(processDataStep2Async)
  .then(processDataStep3Async)

Async/await goes one step further to provide some additional syntactical sugar around the same promise implementation:

data1 = await getSomeDataAsync(data)
data2 = await processDataStep1Async(data1)
data3 = await processDataStep1Async(data2)
data4 = await processDataStep1Async(data3)

Looks lovely! But the implementation of async/await using Promises under-the-hood can result in some leaky abstractions and unexpected behaviors. In this case, I had an async function that did some work and looked sort of like this:

const process = async (data) => {
  let files = await downloadFiles(data)
  return processFiles(files)
}

let result = await process(data)

So far so good. The code waits for the files to download, an asynchronous process, then processes them, and the result is exactly what I expect. But then as my code evolved, I needed to process an array of data instead of a single item, and they need to process in order. No problem, I’ll just iterate over the array with map or forEach, right?

const process = async (data) => {
  return data.forEach(async (item) => {
    let files = await downloadFiles(item)
    return processFiles(files)
  }
}

let result = await process(data)

To my surprise, not only was result not what I expected, but my files didn’t even get downloaded — at least not right away. The problem of course is that forEach is internally not awaiting the result of the async function, so the result is an array of Promises, rather than an array of results.

In order to resolve this issue, I had to implement my own “async forEach”:

const forEachAsync = async (l, f) => {
  for (let idx = 0; idx < l.length; idx++) await f(l[idx], idx, l)
}

Not a big deal, but again, an easy to miss gotcha dealing with async/await. The syntactical sugar of async/await made it easy for me to think it would work, but of course, pausing to consider that it’s using Promises work under-the-hood, it obviously does not work.

React Effects & Stateless Components

This last one is not in the JavaScript language itself, but rather in React — but oh boy are there some banana peels there.

React has recently been making an increasingly firm push to using stateless components, rather than class components. A stateless component is one that is implemented as a single function, without an internal state, and without storing props on the object itself. Instead, the stateless component is just a function which takes in props and returns the rendered content:

// Class component

const Component extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>
  }
}

// Stateless component

const Component = props => {
  return <h1>Hello, {props.name}</h1>
}

This is all well and good — it tends to make for more compact and more simple components. But it also misses out on state and a number of hooks into the React component life-cycle (such as componentDidMount, or componentDidUpdate) which are necessary for some components. In the past, this limited the types of components that could be easily implemented as “stateless”, so React introduced hooks that could (among other things) be used to implement the missing functionality for stateless components.

The most commonly used hooks are the clearly-named “useState” (used to add state to the component) and the opaquely named “useEffect” (used to… do effects, I guess? No, I don’t have a better name, but still, the name is a bit opaque).

Like many others who have written about the topic, I generally like stateless components and hooks, but they can introduce some hidden and unnatural semantics into the behaviors of a component. In particular, there’s a lot of magic happening behind the scenes to enable the “simplicity” of state within a stateless components.

Okay, let’s get to a stripped down example of the code that tripped me up. In this case, I had a component that would trigger a data fetch when it changed:

const Component = props => {
  let getData = (value) => {
    fetch(...)
  }

  return <InputComponent onChange={getData} />
}

That worked well, except of course, that I was hammering my server on every input change. This is a very common issue with this type of pattern, and the typical solution is to debounce the function, which prevents multiple inputs in rapid succession from triggering the fetch. This is something I’ve done hundreds of times–simple!

const Component = props => {
  let getData = _.debounce((value) => {
    fetch(...)
  })

  return <InputComponent onChange={getData} />
}

Except of course, it doesn’t work. The issue is that with a functional component, the entire function body is executed with each render, so the function being returned by debounce is actually a new function each time. To work properly, we need to call the same debounced function each time. This is somehow, at the same time, completely obvious from looking at the code (we’re defining a local variable), and completely non-obvious from a typical JavaScript pattern perspective, particularly coming from a class-based component.

The deeper issue here is that React Effects can introduce an illusion of a stateful component, without actual persistent state. This works fine for the some of the straightforward “useState” or “useEffect” use cases, but can quickly break down when dealing with actual state, as in this example, or when dealing with callbacks or event handlers that deal with objects outside of the component.

In this case, there are of course workarounds to debouncing the fetch call, but the cure may be worse than the disease. In most cases, it involves writing extra code and handling to reconcile the of lifecycle of the stateless component with the world outside of the component. In the quest for simplicity, stateless components & React hooks sometimes result in far more complex code.

Conclusion: Banana Peels Everywhere

Once again, I fully own up to the fact that running into these issues was my own fault. But in each case, I felt like I had been lead down a garden path, right onto a banana peel. Each of these examples was fairly simple to resolve, but if you thought all of these things were obvious, then you’re probably a better JavaScript programmer than I am.