
21 Common JavaScript Mistakes To Avoid
JavaScript is a fairly quirky language, and as such, there are many pitfalls to be avoided when working with it. This is especially true when first starting out.
We’ve compiled a list of 21 of the most common JavaScript mistakes.
In this article, mistake refers to: something done incorrectly that almost certainly can (or will) lead to bugs in your code; or something done in a sub-optimal manner that can be easily improved upon with little effort.
Amongst the mistakes listed, some are fairly east to spot and rectify. Others, however, can be a little more elusive.
Let’s get started.
1. The dreaded NPE (or NullPointerException)
This is the grand-daddy of coding bugs in general. In programming, a NullPointerException
occurs when you attempt to do something like this:
const myVariableInLowerCase = myVariable.toLowerCase()
Where myVariable
is undefined
or null
.
JavaScript won’t actually throw a NullPointerException
technically speaking — though this is what this type of error is commonly referred to as (or NPE, for short).
In JavaScript, this type of erroneous operation will throw a TypeError
(if in Chrome, anyway, it can differ per browser) stating that you Cannot read properties of undefined
.
It’s easy to avoid these type of errors by simply checking the type (or the existence) of the object or variable before invoking some method upon it:
if (typeof myVariable === "string") {
myVariableInLowerCase = myVariable.toLowerCase()
}
You can only use the toLowerCase()
on strings. To verify that myVariable
is indeed a string (and hence, it actually exists/isn’t null
or undefined
) you can use typeof
, as demonstrated here.
If myVariable
wasn’t set, it’s typeof
would be undefined
and we wouldn’t attempt to invoke toLowerCase
upon it at all.
This is a very common JavaScript mistake, but it’s not limited to JavaScript. You’ll see this type of issue across many other programming languages, too.
2. Comparing two objects incorrectly
In JavaScript, comparing objects is not as trivial as using the equals operator directly:
const firstPerson = { name: "Edward", age: 44 }
const secondPerson = { name: "Edward", age: 44 }
console.log(firstPerson === secondPerson) // false!
Comparing objects in this manner is definitely an easy trap to fall into, but unfortunately object comparisons don’t work this way in JavaScript.
In this case, ===
is checking if both objects refer to the same object — and not if all of the attributes are equal between the two objects. Which is probably what you are expecting and/or trying to do.
To compare object attributes, there’s not really an out of the box (native) solution unfortunately, so you’ll need to implement a custom approach instead.
If you’re already using a library like lodash or underscore, however, you’ll already have this mechanism at your disposal (in the form of the isEqual
method, in both cases).
3. Thinking you’re modifying the original item when you aren’t
Some operations modify the original item, other’s don’t.
Let’s use filter
in our example:
const fruits = ["apple", "orange", "plum"]
fruits.filter(fruit => fruit !== "orange")
console.log(fruits) // ["apple", "orange", "plum"]
orange
is not removed from the original array because filter
returns a new array.
If unfamiliar, it’s easy to expect filter
to modify the original array. There are many other examples in JavaScript just like this one.
You just need to be mindful of what is returned by the functionalities you are using. This one is a common JavaScript mistake, but one that is fairly easy to avoid, too.
4. Using setInterval instead of setTimeout (and vice-versa)
It’s easy to use these two timing methods incorrectly without even realizing it.
Remember, setInterval
performs some functionalities repeatedly at specific intervals in time, until it is stopped.
setTimeout
performs it’s functionality only one time after a given interval has passed.
Similarly, a related JavaScript mistake is to use clearInterval
on a timeout
or clearTimeout
on an interval
.
5. Expecting Array.push to return an array with the new value included
If you aren’t sure, it’s always best to verify what is actually returned by the operation you are performing.
For instance when using push
or unshift
on an array:
const myArray = []
myArray.push("orange") // 1
myArray.push("blue") // 2
myArray.unshift("red") // 3
This returns the length of the new array.
You may naturally assume that push
would return the array with the new item included, or the index of the inserted element — but this is not the case.
6. Not understanding strict vs loose comparisons
Here’s another very common pitfall. As far as JavaScript mistakes go — this one’s undoubtedly a classic.
Not understanding the difference between double-equals (==
) and triple-equals (===
).
Double-equals performs a “loose” comparison. So the two items you are comparing can still be considered equal in this comparison even if they are of differing types:
console.log(33 == "33") // true
console.log(true == "true") // true
This is because JavaScript firstly coerces each type before this evaluation (to the same type, if possible).
With triple-equals, this is not the case. The items must also be of the same type, so it’s much stricter in that regard:
console.log(33 == "33") // false
console.log(true == "true") // false
console.log(12 === 12) // true
You’ll definitely want to favour using triple-equals where possible unless you have a specific reason to do otherwise.
It’s well worth in reading up on all of various the nuances in this regard.
7. Not understanding this
JavaScript mistakes caused by an improper (or lack of) understanding of this
and how it works are definitely fairly common.
The concept of this
can be fairly daunting to beginners, and this likely contributes to a lot of the pitfalls in this regard.
The main issue is that this
can (or more specifically, does) refer to different things depending on where it’s used.
For example, used out in the open this
refers to the global object:
console.log(this) // [object Window] or something similar
However, when this
is used in the method of an object:
const car = {
make: "BMW",
mode: "M2",
drive: function() {
// "this" refers to the car object as this
// object is the owner of this function
console.log(this)
}
}
It instead refers to the owning object (car), not the global object anymore.
One of the issues here is it’s hard to even go about debugging these issues if you don’t have a familiarity or understanding of JavaScript’s this
to begin with.
If you’re unfamiliar, I’d definitely recommend brushing up on the other various contexts that this
can be used in.
8. Not understanding variable scope
Variable scope is an important mechanism/concept to understand within JavaScript — there are some nuances to this topic, too.
Here’s a common JavaScript mistake which is especially prominent among beginners:
if (shouldSetName) {
const name = "Rupert"
// or let name = "Rupert"
}
alert("Welcome, " + name) // ReferenceError - name isn't defined
This will lead to a ReferenceError
as const
and let
variables in JavaScript are block-scoped.
In this case, name
was initialized inside the if
statement (block) — so it’s not accessible or visible outside of this particular block.
However, if you were to do this:
if (shouldSetName) {
var name = "Rupert"
}
alert("Welcome, " + name) // no error, name is defined!
This would work as expected. This is because var
variables variables are fundamentally different to const
and let
variables in this regard. var
variables can be accessed outside of the block they were defined in.
This particular example demonstrates what is known as block scope.
There are two other types of scope in JavaScript: global scope and function scope. To read more about block scope as well as these other types of scope, I’d recommend this article in particular.
9. Concatenating (or adding) variables incorrectly
In JavaScript, +
is used to add numbers together as well as to concatenate (join) strings.
This can lead to unexpected outcomes if you aren’t sure that the items you are concatenating (or adding) are of the same type:
const firstNumber = 20
let secondNumber = 25
console.log(firstNumber + secondNumber) // 45
// it got converted to a string somewhere, somehow...
secondNumber = "25"
console.log(firstNumber + secondNumber) // "2025"
This exposes one of the weaknesses of loosely-typed languages like JavaScript, and demonstrates how easy it is to fall prey to these kind of mistakes.
You may assume both firstNumber
and secondNumber
are both actual numbers, then add them together.
In that case, you’ll expect a numeric value in return. But one of the variables is, in fact, a string (secondNumber
) — so you’ll get a string returned instead as JavaScript is actually concatenating both variables together instead of adding them as expected!
10. Not using try/catch
Let’s say you application parses some JSON.
You should be mindful that this parsing operation can throw errors, for example if invalid or incorrect input is encountered.
You can handle these kind of scenarios by wrapping the relevant code in a try/catch
block, like so:
try {
JSON.parse("I cannot be parsed")
} catch (e) {
console.error("An error occurred, handle it accordingly...", e)
}
This will capture any errors that occur during the JSON.parse
operation, and it’ll allow you to gracefully handle or react to these errors within the catch
block.
For instance you can alert the user via the UI that they’ve supplied some invalid input, or something similar.
Many JavaScript mistakes can be avoided or controlled with this kind of forethought, knowing when errors are thrown and making sure to catch them.
11. Treating asynchronous code in a synchronous manner
When working in JavaScript, it’s vitally important to understand how asynchronous code works as opposed to synchronous code.
Here’s an example using the async
/await
approach:
const fakeNetworkRequest = () => {
return new Promise(resolve => {
setTimeout(() => {
console.log("resolved!")
resolve("success")
}, 2000)
})
}
const changeName = async () => {
const result = fakeNetworkRequest()
// incorrect, we aren't awaiting the promise above
console.log("Result received: ", result)
console.log("I'll get logged before the promise even resolves...")
}
Here we’re using the async
keyword when defining the changeName
function.
However, we aren’t await
ing the resolution of the underlying promise within fakeNetworkRequest
.
This means that the subsequent code (just the console.log
s in this case) will likely execute before the promise has even been fulfilled.
So the mistake here is that the developer is expecting this code to run synchronously.
But that’s not how promises work.
const changeName = async () => {
const result = await fakeNetworkRequest()
// correct, the promise has been awaited...
console.log("Result received: ", result)
console.log("The promise has been resolved by the time I am logged")
}
If you await
the promise, you’ll then be able to properly use the results of the promise (in this example, just assume some data has been fetched from the backend or something like that) as expected.
Not treating code that’s meant to run asynchronously in this manner can lead to a whole host of problems and unexpected behaviours!
12. Using map and forEach interchangeably
This one isn’t really a JavaScript mistake, per-se, but it’s worth pointing out in this list.
Consider the following example:
const people = [{ name: "Sarah", age: 40 }, { name: "Peter", age: 33 }]
const ages = []
people.forEach(person => {
ages.push(person.age)
})
There’s nothing inherently wrong with this approach, but you can simplify and condense it a lot by instead using map
:
const ages = people.map(person => person.age)
forEach
performs some functionality for every item in the array.
map
creates a new array directly based on the results of the provided function.
So it can be considered that there is some overlap between these two methods; but map
is much more suitable for this particular use case. It means you don’t need to manually push
each item into the new array (or initialize the new array separately).
13. Using let inappropriately
You should initialize your variables with const
by default (not let
) if you know that the variable in question won’t (or shouldn’t) need to be changed or modified.
This makes your code clearer to other developers and it also makes it less error-prone. The variable cannot be reassigned and thus you can guarantee later on that the variable has actually been initialized with a given type.
let name = "Anne"
// somewhere deep within your application...
// an accident happens
name = 123
// now when you try...
name.toLowerCase() // oops
14. Not using strict mode
Strict mode essentially makes your code less error-prone, and you should realistically always be using it where possible.
To enable safe mode in JavaScript, you can just prepend your JavaScript files with:
use strict;
As the name would imply, it’s a “stricter” mode. It’ll force you to write less buggy and more robust code.
One example of this is the way strict mode handles certain errors.
There are errors in non-strict mode JavaScript that can fail “silently”. So you won’t necessarily be notified that an error has even occurred.
In strict mode, these same errors would now throw exceptions instead.
This means that bugs that may have otherwise been awkward to catch, or may have even gone entirely unnoticed — these bugs will be apparent right away during the development phase, now.
15. Using hyphens in object property names
In comparison to the other JavaScript mistakes on this list, this one is likely not very common. But it’s probably still worth covering anyway.
JavaScript will raise a SyntaxError
if you do this:
const myObject = { some-property: "A property" }
However, you can still do this:
const myObject = { "some-property": "A property" }
This isn’t recommended, however, as it can lead to numerous problems.
Let’s assume you want to access some-property
, for instance:
console.log(myObject.some-property)
This will throw a ReferenceError
stating property is not defined
. The hyphen is interfering!
You can still actually access some-property
using this notation, instead:
console.log(myObject["some-property"])
But it’s recommended to simply not use hyphens in property names to save any headaches later on!
16. Putting a lot of complex code in to one function
Strictly speaking, this isn’t necessarily a JavaScript mistake per-se. But it’s definitely a pitfall that many developers can fall into, so it’s worth covering here.
It’s better to have smaller, reusable functions which can be used as building blocks, not massive functions that contain lots of complex, intertwined business logic.
There are several benefits to keeping your functions small and modular:
- The code in general is much easier to maintain as it’ll ultimately become less complex and less daunting
- It’ll hopefully be easier to see where bugs are coming from and how to go about more effectively resolving these bugs
- You can leverage existing code and reuse it instead of writing the same code over and over
If you have a function that includes some logic to break apart a string (somebody’s full name) and capitalize the first letter of each portion of this name, for example, this would be a classic case for a utility function.
The same utility function (formatName
) could then be used freely and easily anywhere else across the codebase without any effort.
That’s a trivial example. But in general you should think with this mindset.
Keep you functions clean and try to give them very specific responsibilities.
17. Not discerning between arrow functions and “regular” functions – they are different!
It’s a common JavaScript mistake to assume that arrow functions and regular functions are technically identical, with only syntactical differences.
This is not the case, however.
There are several differences between these two types of function, the most prominent one is perhaps the behaviour of this
:
// the global window object
const myFunc = () => console.log(this)
With arrow functions, this
is inherited from the outer function. They don’t really have a this
of their own.
This can be problematic for several reasons. Take this example:
const car = {
make: "BMW",
model: "M3",
getMakeAndModel: function() {
// "this" refers to the car object as this
// object is the owner of this function
console.log(this.make + " " + this.model) // BMW M3
}
}
Now if you were to convert the getMakeAndModel
method to an arrow function instead of a regular function:
const car = {
make: "John",
mode: "Doe",
getMakeAndModel: () => {
// "this" now refers to the global window object
console.log(this.make + " " + this.model) // undefined undefined
}
}
this
refers to the global window object!
So as demonstrated, the global window object doesn’t contain the make
and model
properties we are looking for — they are undefined
when we log them out.
This is only one of the key differences with regards to how arrow functions behave versus regular functions, but I’d say this is definitely the one that’s most likely to lead to mistakes in your JavaScript code!
18. Leaving in unwanted console.logs
If you’re working with other developers on production code, it’s likely that you won’t want console.log
s leaving in place. There are exceptions to this, but generally, in production code, you won’t want the logs.
After debugging locally, be mindful of this, and remember to clear up those leftover logs!
19. Bad (or inconsistent) code formatting and style
Inconsistent code formatting or code style is not the end of the world, but it’s best to be consistent and keep everything neat and tidy across the board where possible.
This is especially true when working as part of a team. In this case, it’s definitely a good idea to implement and configure a code formatter like Prettier.
This integrates with your IDE or editor, and it means that all contributors can maintain a consistent code style. Prettier (and other tools like it) enforce a specific code style. This code style can of course be configured and tweaked by the team as a whole as per the agreed preferences.
This simply saves a lot of time and keeps the project neat and tidy. It also removes the requirement for one extra step (verifying that all of your brackets are lined up, for example, or verifying that there are no extra empty lines) before committing your code.
20. Accidentally assigning instead of comparing
This one’s short and sweet. But a JavaScript mistake most of us will no doubt encounter at one time or another:
let name = "Rachel"
if (name = "Tom") {
console.log("Hi Tom!")
}
There’s a simple typo here. We’re using =
instead of ==
(or more preferably, ===
).
This means that in this case, the if
statement will always evaluate to true, and subsequently Hi, Tom!
will always be logged to the console.
Not only that, we’ve actually reassigned name
(to Tom) now, also!
21. Forgetting about zero-indexing
JavaScript, like a lot of other languages, counts from 0. Not from 1, as some beginners may wrongly suspect.
So accessing the first item in the array can be achieved like so:
const colours = ["blue", "pink", "yellow"]
colours[0] // blue
And not like so:
colours[1] // pink
This would give us the second element, as JavaScript counts from 0.
In conclusion
And that’s the end of the list
We hope you’ve enjoyed our glimpse at these more common JavaScript errors, hopefully you’ve learned something new in the process!
There are 21 errors or mistakes here, but this is only scratching the surface.
Check back later for follow-up articles as well as other “common mistake” articles spanning different domains (React, Node and so on).
For other JavaScript related content, do feel free to check out out JavaScript category in the meantime.