Things to avoid in JavaScript

Published on

Things to avoid in JavaScript

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('&', "&amp;") .replaceAll('<', "&lt;") .replaceAll('>', "&gt;") .replaceAll('"', "&quot;") .replaceAll("'", "&#39;") ?? ''; } console.log(escapeHtml('<button>456</button>')); // &lt;button&gt;456&lt;/button&gt;

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():

  1. JSON.stringify() doesn't support some values, such as NaN or undefined. They can be skipped or converted into null. For some data types, such as bigint, it will even throw exception.
  2. JSON.stringify() cannot work with objects that contain cyclic references: const obj = {}; obj.selfReference = obj; console.log(JSON.stringify(obj)); // exception
  3. 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:

  1. Dynamically changing the object negatively affects the performance, because objects are optimized to have a fixed structure (or shape).
  2. 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 to null (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); // &lt;1&gt;<div>&lt;2&gt;&lt;3&gt;</div>&lt;4&gt;

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:

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:

Holy Trinity
JavaScript's "Holy Trinity"

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.





Read previous


This site uses cookies. By continuing to use this website, you agree to their use. To find out more, including how to control cookies, see here: Privacy & cookies.