Casper Beyer Self-proclaimed developer advocate, hate slow software. Grew up with C, work with JavaScript and a fan of Go.

Common JavaScript “gotchas”

5 min read 1500

JavaScript has been getting a lot of new, sugary features ever since we got over Harmony, while more features can allow us to write readable, high-quality code it’s also easy to go overboard with what’s new and shiny and run into some of the potential pitfalls.

Let’s go over some of the “gotchas” I see come up quite frequently as sources of confusion, both new and old.

Arrow functions and object literals

Arrow functions provide a terser and shorter syntax, one of the features available is that you can write your function as a lambda expression with an implicit return value. This comes in handy for functional style code, like when you have to use mapping arrays using a function. That would be quite a few empty lines with regular functions.

For example:

const numbers = [1, 2, 3, 4];
numbers.map(function(n) {
  return n * n;
});

Becomes a sleek easy to read one-liner with the lambda style arrow functions:

const numbers = [1, 2, 3, 4];
numbers.map(n => n * n);

This use case of an arrow function will work as one would expect, it multiplies the values by itself and returns to a new array containing [1, 4, 9, 16].

However, if you try mapping into objects however the syntax isn’t what one might intuitively expect it to be, for example, let’s say we’re trying to map our numbers into an array of objects containing the value like this:

const numbers = [1, 2, 3, 4];
numbers.map(n => { value: n });

The result here will actually be an array containing undefined values. While it might look like we are returning an object here, the interpreter sees something completely different. The curly braces are being interpreted as the block scope of the arrow function, the value statement actually ends up being a label. If we were to extrapolate the above arrow function into what the interpreter actually ends up executing, it would look something like this:

const numbers = [1, 2, 3, 4];
numbers.map(function(n) {
  value:
  n
  return;
});

The workaround is quite subtle, we just need to wrap the object in parenthesis which turns it into an expression instead of a block statement, like this:

const numbers = [1, 2, 3, 4];
numbers.map(n => ({ value: n }));

Will evaluate to an array containing an array of objects with the values as one would expect.

Arrow functions and bindings

Another caveat with arrow functions is that they don’t have their own this binding, meaning their this value will be the same one as the this value of the enclosing lexical scope.

So despite the syntax being arguably “sleeker” arrow functions are not a replacement for good ‘ol functions. You can quickly run into situations where your this binding is not what you thought it was.

For example:

let calculator = {
  value: 0,
  add: (values) => {
    this.value = values.reduce((a, v) => a + v, this.value);
  },
};
calculator.add([1, 2, 3]);
console.log(calculator.value);

While one might expect the this binding here to be the calculator object there, it will actually result in this being either undefined or the global object depending on if the code is running in strict mode or not. This is because the closest lexical scope here is the global scope, in strict mode that is undefined, otherwise, it’s the window object in browsers (or the process object in a Node.js compatible environment).

Regular functions do have a this binding, when called on an object this will point at the object so using a regular function is still the way to go for member functions.

let calculator = {
  value: 0,
  add(values) {
    this.value = values.reduce((a, v) => a + v, this.value);
  },
};
calculator.add([10, 10]);
console.log(calculator.value);

Also, since an arrow function has no this binding Function.prototype.call, Function.prototype.bind and Function.prototype.apply won’t work with them either. The this binding is set in stone when the arrow function was declared and can’t change.

So in the following example will run into the same issue as we had earlier, the this binding is the global object when the adder’s add function is called despite our attempt to override it with Function.prototype.call:

const adder = {
  add: (values) => {
    this.value = values.reduce((a, v) => a + v, this.value);
  },
};
let calculator = {
  value: 0
};
adder.call(calculator, [1, 2, 3]);

Arrow functions are neat, but they can’t replace regular member functions where a this binding is needed.

Automatic semicolon insertion

While it’s not a new feature, automatic semicolon insertion (ASI) is one of the weirder features in JavaScript so it’s worth a mention. In theory, you can omit semicolons most of the time (which many projects do). If the project has a precedent you should follow that but, you do however need to be aware that ASI is a feature or you’ll end up having code that can be deceiving.

Take the following example:

return
{
  value: 42
}

One might think it would return the object literal, but it will actually return undefined because semicolon insertion takes place making it an empty return statement, followed by a block statement and a label statement.

In other words, the final code that is actually being interpreted looks more like the following:

return;
{
  value: 42
};

As a rule of thumb, never start a line with an opening brace, bracket or template string literal even when using semicolons because ASI always takes place.

Shallow sets

Sets are shallow, meaning duplicate arrays and objects with the same values which will lead to multiple entries in the set.

For example:

let set = new Set();
set.add([1, 2, 3]);
set.add([1, 2, 3]);

console.log(set.length);

The size of that set will be two, which makes sense if you think of it in terms of references as they are different objects.

Strings are immutable, however, so multiple strings in a set like this:

let set = new Set();
set.add([1, 2, 3].join(','));
set.add([1, 2, 3].join(','));
console.log(set.size);

Will end up with the set having a size of one because strings are immutable and interned in JavaScript which can be used as a workaround if you find yourself needing to store a set of objects one could serialize and de-serialize them instead.

Classes and the temporal dead zone

In JavaScript, regular functions get hoisted to the top of the lexical scope, meaning the example below will work as one might expect:

let segment = new Segment();

function Segment() {
  this.x = 0;
  this.y = 0;
}

But the same is not true for classes, classes are actually not hoisted and need to be fully defined in the lexical scope before you attempt to use them.

For example:

let segment = new Segment();

class Segment {
  constructor() {
    this.x = 0;
    this.y = 0;
  }
}

Will result in a ReferenceError when trying to construct a new instance of the class because they aren’t hoisted like functions are.

Finally

Finally is a bit of a special case, take a look at the following snippet:

try {
  return true;
} finally {
  return false;
}

What value would you think it returns? The answer is both intuitive and, at the same time, can become unintuitive. One could think the first return statement makes the function actually return and pop the call stack, but this is the exception to that rule because finally statements are always run so the return statement inside the finally block returns instead.

In conclusion

JavaScript is easy to learn but hard to master, in other words, it’s error-prone unless a developer is careful about what and why they’re doing something.

This is especially true with ECMAScript 6 and its sugary features, arrow functions, in particular, come up all of the time. If I was to make a guess I’d say it’s because developers see them as being prettier than regular functions, but they’re not regular functions and they can’t replace them.

Skimming the specification from time to time doesn’t hurt. It’s not the most exciting document in the world but as far as specifications go it’s not THAT bad.

Tools like the AST Explorer also help in shedding some light in what is going on in some of these corner cases, humans and computers tend to parse things differently.

With that said, I’ll leave you with this final example as an exercise.

Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Try it for free.

Casper Beyer Self-proclaimed developer advocate, hate slow software. Grew up with C, work with JavaScript and a fan of Go.

Leave a Reply