Introduction: Why is JavaScript a Minefield for Beginners?
JavaScript is a language that is simultaneously easy to get started with and difficult to deeply understand. Many novice developers, when coming to Node.js, encounter unexpected bugs that seem irrational. The problem often lies not in the logic of the algorithm, but in fundamental features of the language: asynchronicity, the non-obvious behavior of this, and the subtleties of scopes.
In this article, we will break down the most common mistakes that beginners make in JavaScript (using Node.js as an example). Each point will be accompanied by real code and an explanation of how to fix it. If you want to write reliable code and not spend hours on debugging, this material is for you.
1. Scope Confusion: var, let, and const
One of the most common problems is using the outdated var and not understanding the difference between let and const.
The Problem with var and Hoisting
Variables declared with var are "hoisted" to the top of the function but remain undefined until the point of initialization. This leads to hard-to-find bugs.
// ❌ Error: undefined instead of ReferenceErrorconsole.log(myVar); // undefined (no error!)var myVar = 5;
// ✅ Correct: let or constconsole.log(myLet); // ReferenceError: Cannot access 'myLet' before initializationlet myLet = 5;Closures in Loops
A classic mistake with var in loops:
// ❌ Incorrect: all callbacks will output 3for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100);}// Output: 3, 3, 3
// ✅ Fix: let creates a new binding for each iterationfor (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100);}// Output: 0, 1, 2Tip: Forget about var in modern JavaScript. Use const by default and let if the variable needs to be reassigned.
2. Async Hell: Callback Hell and Incorrect Promise Handling
Node.js is built on asynchronicity. Beginners often try to get the result of an asynchronous function synchronously, which leads to undefined.
Ignoring Promises
// ❌ Error: function returns undefinedfunction getUserData(userId) { const user = database.findUser(userId); // Asynchronous operation return user; // undefined, because the data hasn't arrived yet}
// ✅ Correct: return a promiseasync function getUserData(userId) { const user = await database.findUser(userId); return user;}Callback Hell
Nested callbacks make code unreadable and difficult to maintain:
// ❌ Bad: pyramid of callbacksfs.readFile('a.txt', (err, dataA) => { fs.readFile('b.txt', (err, dataB) => { fs.writeFile('c.txt', dataA + dataB, (err) => { console.log('Done!'); }); });});
// ✅ Good: async/awaitasync function mergeFiles() { const dataA = await fs.promises.readFile('a.txt'); const dataB = await fs.promises.readFile('b.txt'); await fs.promises.writeFile('c.txt', dataA + dataB); console.log('Done!');}Golden Rule: If you see nesting deeper than 2 levels, refactor using async/await.
3. The Mysterious this: Losing Context
In JavaScript, this is determined by how a function is called, not where it is declared. This is the main cause of confusion.
Losing this in Object Methods
const user = { name: 'Alex', greet() { console.log(`Hello, ${this.name}`); }};
// ✅ Worksuser.greet(); // Hello, Alex
// ❌ Error: this is lost