Most of what people do on the web, from viewing a website to using a dashboard to running a mobile app or a back-end service, uses JavaScript. Despite this, many new developers say that learning JavaScript can be confusing, unpredictable and even “strange”.
The usual reason for that description isn’t due to the language, itself, but the way developers learn about it.
A small amount of misunderstanding in terms of the basic behaviors of the language — such as scope, type coercion or asynchronous behavior — can result in bugs that are seemingly random and unexplainable. Those bugs are then compounded over time and cause new developers to lose confidence in their ability to write reliable code. That loss of confidence causes them to believe that JavaScript is an inherently unstable programming language.
Actually, JavaScript has a lot of consistency. The problem is it’s very permissive. Once you know the rules JavaScript follows, the behavior of the language is easily predictable and easy to use. This article will detail the most common pitfalls that new developers face when working with JavaScript, explain why they occur and provide some real-world ways to prevent them.
Understanding scope: why variables don’t always stay where you expect
One of the earliest surprises for JavaScript developers is how variable scope is used.
Prior to ES6, any variables that were declared using the “var” keyword were defined as being scoped at the function level, and not block-scoped. This means that any variable declared inside an if statement or switch statement, etc., could still be referenced outside of those statements.
if (true) {
var count = 5;
}
console.log(count); // 5 — still accessible
Many times this behavior has caused unintended side effects from many developers who have assumed that any variables declared within certain types of blocks (i.e. loops and conditionals) would never be accessible after leaving the confines of said blocks.
A safer default
JavaScript has made things much clearer for all developers now.
if (true) { const count = 5; } console.log(count); // ReferenceError
By defaulting to using “const”, which is only meant to be reassigned when explicitly needed; will introduce block scoping and make variables act like most developers expect them to act. By implementing this one simple habit, you will eliminate an entire category of bugs that are very difficult to track down.
Asynchronous code doesn’t run in order — and that’s intentional
Asynchronous code does NOT follow the order that you write in your code — and that’s exactly what we want
JavaScript has one thread (or path) of execution at any given time, but JavaScript is ultimately based on asynchronous processes. A network call, timer, or input/output operation will stop the execution of your program.
Here is a typical example of how beginners might write their asynchronous code.
let data; fetch('/api/data') .then(response => response.json()) .then(json => data = json); console.log(data); // undefined
While nothing is wrong with this piece of code; the console.log statement fires off before the completion of the network call which means there is no way for “data” to have been set at that point.
Thinking in async flows
Using Async/Await can help us make our async flows much more clear.
async function loadData() { const response = await fetch('/api/data'); const data = await response.json(); console.log(data); } loadData();
Experienced developers think asynchronously by default. Rather than viewing asynchronous behavior as the exception to the rule, they view it as the norm. Experienced developers experience less confusion because of this thinking pattern and are able to reason through very complex systems much easier — a principle that can be applied equally well outside of coding, where clarity and consistency result in fewer ambiguities in complex systems.
Objects and arrays are referenced, not copied
One of the most confusing aspects of JavaScript has to do with how it handles non-primitive values.
JavaScript passes objects and arrays by reference (not by value). Therefore, when you assign one variable to another you are not creating a copy of that object. Instead, both variables point to the exact same object stored in memory.
Therefore, when you make a change using either variable you will be making the same change to the object referenced by the other variable.
const a = { x: 1 }; const b = a;
b.x = 2; console.log(a.x); // 2
Being explicit about intent
If you intend for the two variables to reference different objects, you must create those objects explicitly.
const a = { x: 1 }; const b = { ...a }; // shallow copy
b.x = 2; console.log(a.x); // 1 — original unchanged
Early understanding of the differences between a shallow and deep copy will save you from the very difficult-to-debug “state” issues that arise from sharing references, as well as from the many long lived objects in your application.
Loose equality hides logic errors
The loose equality operator of JavaScript (==), is a type coercive operation. The convenience of this may lead to unexpected behavior.
0 == '0'; // true false == '0'; // true
Loose equality can hide problems with your code logic, and create situations where code works only by accident.
Prefer explicit comparisons
Strict equality (===) requires you to be precise.
0 === '0'; // false Number('0') === 0; // true — explicit conversion
Expressing conversion explicitly makes for readable, predictable and sustainable coding practices — all characteristics that are important to the ability of humans and computers to understand the context of systems, such as how generative systems use clear signaling and intent.
Copy-pasting logic instead of extracting it
All too often the “pitfalls” we encounter are not necessarily language specific. One of the most common bad coding practices which gradually degrades our codebase’s quality is simply copying-and-pasting the same logic (instead of encapsulating shared behavior).
This behavior will ultimately result in:
Anytime the same logic appears more than once in your code, take a moment to ask yourself:
Can I create a method for this? Can I create a small utility for this?
Early Refactoring Encourages Cleaner Structure and Prevents Unnecessary Complexity From Developing.
What beginners often misunderstand
Behavior like this is typically viewed as an exception or an anomaly by many. However, in actuality, this is fundamental to how JavaScript functions:
However, once you fully comprehend the above rules, JavaScript is much more predictable. The confusion goes away not because the language itself has changed; it is simply because your mental model of the language has improved.
Practical habits that pay off early
In teams and across projects, there are some habits that have been shown to produce better results for developers:
While these practices do provide value for developers who are new to JavaScript, they are also important for developing frameworks, libraries, and large-scale systems that are easier to reason through as complexity increases.
Conclusion — building a reliable JavaScript foundation
This permissiveness can be a huge strength for developers who understand the underlying rules it can be very frustrating for those who do not. By treating Scope, References, Asynchronous Execution, and Type Behavior Seriously At The Onset of Development, Developers Will Avoid Countless Small Bugs, Significantly Reduce Debugging Time, And Create A Better Foundation For Learning Advanced Patterns and Frameworks Over Time.
Good JavaScript isn’t about memorizing a list of “quirks” it is about comprehending the rules that subtly define daily behavior.
If this article helped clarify patterns you’ve run into previously, please feel free to share it with your coworkers or others within your social circle. If you’re interested in even more practical, developer-centric insight into how complex systems behave both in development and outside of it you can subscribe to the Sonnet eNewsletter.
With decades of experience and a dedicated team, we are committed to delivering high-quality web development services. Our client-centric approach ensures that we understand your needs and provide solutions that exceed your expectations.