Things to avoid in JavaScript
Published on

Many inexperienced (or even not so) frontend and backend developers do mistakes which can lead to bugs or reliability issues. Here I am going to list the things that should probably be avoided to reduce the risk of errors. Everything I list here are not absolutes, there are definitely situations where they might be fine or even necessary. This is more of an advise.
Using element.innerHTML to set the visible text
I've seen countless times where developers used
element.innerHTML
to change the visible text in DOM elements. This might work incorrectly if the text is not an escaped HTML code:
document.body.innerHTML = '<button>456</button>' // will be a button with text "456"
The result will be this: .
In order to fix this use
element.innerText
,
or even better
element.textContent
,
since the latter doesn't process the styles (for example skipping text in hidden elements) and has better
performance (when getting the value):
document.body.textContent = '<button>456</button>' // will be text "<button>456</button>"
The result will be: <button>456</button>, as we expected.
If you need to escape HTML in environments with no DOM API, such as NodeJS, you can do this:
function escapeHtml(htmlStr) {
return htmlStr?.toString()
.replaceAll('&', "&")
.replaceAll('<', "<")
.replaceAll('>', ">")
.replaceAll('"', """)
.replaceAll("'", "'") ?? '';
}
console.log(escapeHtml('<button>456</button>')); // <button>456</button>
Using JSON.parse(JSON.stringify(object)) to clone objects
Many of us have used JSON.stringify()
and then passed the resulting string to
JSON.parse()
in order to deeply copy the object. This approach might be fine for simple cases,
however, many
don't realize the potential limitations of JSON.stringify()
and JSON.parse()
.
This is where things can go wrong when doing JSON.stringify()
/ JSON.parse()
:
-
JSON.stringify()
doesn't support some values, such asNaN
orundefined
. They can be skipped or converted intonull
. For some data types, such asbigint
, it will even throw exception. -
JSON.stringify()
cannot work with objects that contain cyclic references:const obj = {}; obj.selfReference = obj; console.log(JSON.stringify(obj)); // exception
- Although usually not as serious as the first 2, but I must say it's not efficient for larger objects. It's slow and wastes a lot of memory.
structuredClone()
should be preferred as much as possible. structuredClone()
also
automatically handles self referencing / cyclic structures.
const obj = {};
obj.selfReference = obj;
const clonedObj = structuredClone(obj);
console.log(obj === clonedObj);
// false, because it's a cloned object with a different memory address
console.log(clonedObj.selfReference === clonedObj);
// true, because it has the same structure as obj (isomorphic to obj, i.e. as a graph)
For more advanced information about object cloning and comparison, please read this.
Comparing objects by using JSON.stringify()
This is related to the previous one. In addition to failing JSON.stringify()
, the comparison might
still work incorrectly because the order of the properties might be different:
const obj1 = {a: '1', b: '2'};
const obj2 = {b: '1', a: '2'};
console.log(JSON.stringify(obj1)); // {"a":"1","b":"2"}
console.log(JSON.stringify(obj2)); // {"b":"1","a":"2"}
For deep comparisons is libraries like Lodash or read this
Overusing regular objects as associative arrays
Sometimes it's fine to use objects as fixed associative arrays when no new items are added or updated. However it's a different story when you want a modifiable associative array with dynamic keys or huge number of items. This will create 2 problems:
- Dynamically changing the object negatively affects the performance, because objects are optimized to have a fixed structure (or shape).
-
By default, JavaScript objects have one property that behaves very differently. That property is
Object.prototype.__proto__
.Object.prototype.__proto__
is a getter and setter that gets or sets the object prototype. And this fact makes it dangerous to use objects as key-value maps, because property"__proto__"
will behave very differently."__proto__"
is a source of nasty security issues called "prototype pollution".Here are examples of where setting or getting
"__proto__"
will cause problems:const obj = {}; obj["key1"] = "aaa"; console.log(obj["key1"]); // "aaa", fine obj["key2"] = "bbb"; console.log(obj["key2"]); // "bbb", fine obj["__proto__"] = "ccc"; console.log(obj["__proto__"]); // { ... }, if __proto__ is set to a non object, nothing will happen obj["__proto__"] = { a: 5 }; console.log(obj["a"]); // 5, the object inherits the properties of __proto__ obj["__proto__"] = obj; // Exception, because of circular proto chain
One way of fixing this is to get rid of
__proto__
by setting it tonull
(as a bonus this will also get rid of other properties and methods, leaving a much purer object):const obj = { __proto__: null }; obj["key1"] = "aaa"; console.log(obj["key1"]); // "aaa", fine obj["key2"] = "bbb"; console.log(obj["key2"]); // "bbb", fine obj["__proto__"] = "ccc"; console.log(obj["__proto__"]); // "ccc", now it's fine obj["__proto__"] = { a: 5 }; console.log(obj["a"]); // undefined, now the object doesn't inherit the properties obj["__proto__"] = obj; // No exception, circular references work fine
However, considering these problems, you should better to use
Map
instead of regular objects.
Using eval() for code execution
eval()
is a terrible way to execute
code, and you
probably shouldn't use it. The reason is because eval()
can also access the local
variables, which means it's less safe and also makes it to run slower:
function f() {
let a = 1;
eval("++a");
console.log(a); // 2
}
f();
Use
Function
instead:
{
// A simple function without arguments
const f1 = new Function('console.log("f1")');
// A function with arguments and return statement
const f2 = new Function('a', 'b', 'return a * b;');
// A function with default arguments and return statement
const f3 = new Function('a = 1', '{b = 2, c = 3} = {}', 'd = 4', 'return [a, b, c, d];');
let a = 1;
// A function that attempts to access a local variable
const f4 = new Function('++a');
f1(); // "f1"
console.log(f2(6, 8)); // 48
console.log(f3()); // [1, 2, 3, 4]
console.log(f3(10, { c: 30 })); // [10, 2, 30, 4]
f4(); // error since "a" is not a global variable
}
Warning, any code execution has potential risks, and you should never execute any untrusted code.
Appending something to element's innerHTML, innerText or textContent
When you append something to some element's innerHTML
, innerText
or
textContent
by doing something like element.innerHTML += 'some html code'
, all the existing
children will be removed and the new children will be created from scratch. This will be much slower and also
can clear
the
existing children's states, event listeners, etc.
The correct way of appending / prepending is using
element.insertAdjacentHTML()
or
element.insertAdjacentText()
:
const parent1 = document.createElement('div');
const parent2 = document.createElement('div');
const child1 = document.createElement('div');
const child2 = document.createElement('div');
parent1.append(child1);
parent2.append(child2);
console.log(parent1.innerHTML); // <div></div>
console.log(parent2.innerHTML); // <div></div>
child1.insertAdjacentHTML('beforebegin', '<b>1</b>'); // adding html before the element
child1.insertAdjacentHTML('afterbegin', '<b>2</b>'); // adding html to the beginning of the element
child1.insertAdjacentHTML('beforeend', '<b>3</b>'); // adding html to the end of the
child1.insertAdjacentHTML('afterend', '<b>4</b>'); // adding html after the element
child2.insertAdjacentText('beforebegin', '<1>'); // adding text before the element
child2.insertAdjacentText('afterbegin', '<2>'); // adding text to the beginning of the element
child2.insertAdjacentText('beforeend', '<3>'); // adding text to the end of the element
child2.insertAdjacentText('afterend', '<4>'); // adding text after the element
console.log(parent1.innerHTML); // <b>1</b><div><b>2</b><b>3</b></div><b>4</b>
console.log(parent2.innerHTML); // <1><div><2><3></div><4>
Not using modules
It's 2025, and mainstream browsers have supported JavaScript modules for more
than 7 years. Including <script>
tags without attribute type="module"
is largely
outdated. type="module"
tells the browsers to load the script as a module. Modules have several
advantages:
- Encapsulation. Globally declared variables, classes or functions inside modules don't pollute the global context.
-
Controlling what to import or export. With keywords
import
andexport
you can specify what can be loaded and what we want to load. Also you can even load a module dynamically by usingimport()
syntax. - Being loaded once even when imported multiple times. This means no duplicate script loading and execution.
- Being executed only after the other imported modules and the main document are fully loaded. So you don't have to worry about missing dependencies or incomplete document. Thanks to top-level await, this will work even when you do asynchronous initialization (like fetching some resource) in some modules.
- Strict mode. The code runs completely in strict mode inside modules.
Give preference to modules for the main source code of your site.
Using var
There was a time when using let
and const
caused significant performance penalties. The reason is that JavaScript has to make sure that the variables
declared by
let
or const
can only be accessed after their declarations, thus this forced the
JavaScript compilers to do additional checks (temporal dead zone (or TDZ in short) checks ) when such variables
are being accessed. The most famous example was
the
10%
of performance improvement when let
or const
was replaced with
var
.
Even today let
or const
can cause some overhead, but in the recent years JavaScript
engines have done tremendous optimizations which allowed to avoid TDZ checks in many situations. So, using
var
is doing more harm than good in the vast majority of the situations because of its poor
scoping. Switch to var
only if it's a performance-sensitive code and only when the benchmarks
indeed show
performance improvements with var
.
Using loose equality or inequality (== or !=)
We can't even count the number of memes about the insanity of loose equality
(==
) in JavaScript.
One of the funniest memes is this "Holy Trinity" of loose comparisons:

Using ==
or !=
instead of ===
or !==
sometimes
causes unexpected behaviors. So give preference to ===
or !==
unless you are 100% sure what you are doing.
String concatenation with operator + when inserting some values
When doing string concatenation to insert some values prefer using interpolation with template literals
(`... ${some value} ...`
) when it's possible.
This will make the code more readable. So, instead of this:
const name = prompt("What is your name?");
alert("Hi, " + name + ", nice to see you!");
Better to do this:
const name = prompt("What is your name?");
alert(`Hi, ${name}, nice to see you!`);
Conclusion
JavaScript is constantly evolving and improving, new features are added to improve JavaScript's ergonomics and security. While it's not required to update the old code in order to make it work on the newest browsers or the latest versions of NodeJS, the developers should always keep an eye on new features so that they can apply better programming patterns and write more readable code by using these features.