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

Common JavaScript “gotchas”

4 min read 1368

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.

We made a custom demo for .
No really. Click here to check it out.

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.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    : Debug JavaScript errors easier by understanding the context

    Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

    LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

    LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

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

    Leave a Reply