9 Common Mistakes Made by JavaScript Programmers

Alex was notified of a page crash and promptly discovered that the server was sending data in the wrong format. Instead of an empty array, it was returning null, resulting in a forEach method error that caused the page to go blank. When he pointed out that it was a server-side error, the testing team countered, “Even if the server returns incorrect data, the page shouldn’t just go blank!” Alex grudgingly fixed the issue, lamenting that the blame always seems to fall on the frontend.
Soon after, Mike flagged down Alex, complaining that his component wasn’t rendering. Alex, clearly annoyed, noted that Mike had provided an object instead of the expected array for the ‘data’ attribute. Mike contended, “Even with the wrong data format, the component should still show something, like a ‘no data’ message.” Alex, feeling resigned, fixed the issue while venting about the situation.
Alex often found himself in such situations, and gradually, his colleagues started to question his technical abilities. When he heard these murmurs, Alex felt wronged; it seemed like he was always blamed for other people’s mistakes.
When Alex left the company, I took a look at his code and honestly, it was subpar and easily broken. Let’s delve into a critique of his work.
1. Variable Destructuring Error on First Attempt
Before optimization:
const App = (props) => {
const { data } = props;
const { name, age } = data;
}If you see no issues with the code above, I must say your grasp of variable destructuring assignment is not very solid.
The rule of destructuring assignment is that if the value on the right side of the equals sign is not an object or array, it should be converted to an object first. Since
undefinedandnullcannot be converted into objects, destructuring them will result in an error. Therefore, ifdataisundefinedornull, the code above will throw an error.
After optimization:
const App = (props) => {
const { data } = props;
const { name, age } = data || {};
}This optimized code includes a fallback to an empty object if data is undefined or null, preventing the destructuring assignment from throwing an error. This is a common pattern used to ensure that your component doesn't break when expected properties are missing from the props.
2. Unreliable Default Values
Some of you might look at the previous section and think the code could be further optimized. Here’s an additional optimization attempt:
const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data;
}However, shaking my head, I must say that your understanding of ES6 default values is not quite solid.
ES6 uses the strict equality operator (===) to determine whether a variable has a value. Therefore, default values will not take effect if an object's property value is anything other than strictly equal to undefined.
Hence, if props.data is null, then const { name, age } = null will indeed throw an error!
3. Array Methods Require Actual Arrays
Before optimization:
const App = (props) => {
const { data } = props;
const nameList = (data || []).map(item => item.name);
}Now, here comes the problem. If data is the number 123, the result of data || [] is 123. Since 123 is a number, it does not have a map method, which will cause an error. Array methods can only be called on real arrays, and even 'array-like' objects won't suffice. To reliably determine if data is a true array, Array.isArray is the most dependable method.
After optimization:
const App = (props) => {
const { data } = props;
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}The optimized code safeguards against non-array data types by using Array.isArray before attempting to use array-specific methods like map. This ensures that nameList is assigned only when data is a true array, preventing any potential errors from incorrect data types.
4. Not Every Item in an Array Is Necessarily an Object
Before optimization:
const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `My name is ${item.name}, and I am ${item.age} years old.`);
}
}If any item in the data array happens to be undefined or null, then item.name will definitely cause an error, potentially leading to another blank screen.
After optimization:
const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `My name is ${item?.name}, and I am ${item?.age} years old.`);
}
}The optional chaining operator ? is convenient to use but should not be overused. item?.namegets compiled into item === null || item === void 0 ? void 0 : item.name, and overusing it can lead to increased code size after compilation.
Second optimization:
const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `My name is ${name}, and I am ${age} years old.`;
});
}
}This second optimization accounts for the possibility that not every item in the array is an object. It provides a fallback to an empty object, ensuring that destructuring doesn’t result in an error, and the application continues to run smoothly without crashes.
5. Who Can Call Object Methods
Before optimization:
The original code attempts to get the keys of the data object using Object.keys(data). This approach assumes that data can be converted into an object, which is not the case for undefined or null. Using object methods on these values will result in an error.
const App = (props) => {
const { data } = props;
const nameList = Object.keys(data);
}After the first optimization:
The code is updated to include a fallback to an empty object when data is undefined or null, preventing errors when calling Object.keys.
const App = (props) => {
const { data } = props;
const nameList = Object.keys(data || {});
}After the second optimization:
The code introduces a function isPlainObject to check if data is a plain object (i.e., not null, undefined, or any other non-object type) before attempting to use Object.keys. This provides a more robust check and avoids errors related to inappropriate usage of object methods.
const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props;
let nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}The second optimization ensures that Object.keys is only called on valid objects, providing a safer way to handle data and preventing potential runtime errors.
6. Error Handling with async/await
Before optimization:
The initial version of the code sets a loading state before and after fetching data with an asynchronous function queryData(). However, if queryData() throws an error, the loading state might never be reset, causing the interface to remain in a loading state indefinitely.
import React, { useState } from 'react';
const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}After optimization:
The updated code includes a try/catch block to handle any errors that might occur during the data fetching process. This ensures that the loading state is properly reset, regardless of whether queryData() succeeds or fails.
import React, { useState } from 'react';
const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
} catch (error) {
// Error handling can be done here
} finally {
setLoading(false);
}
}
}Second optimization:
For a more elegant approach to capturing errors with async/await, the code can utilize the await-to-js utility. This library allows you to handle errors without the need for a try/catch block, making the code cleaner and easier to read.
import React, { useState } from 'react';
import to from 'await-to-js';
const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
// Handle the error if there is one
// Handle the success response if there is no error
setLoading(false);
}
}This second optimization simplifies the error handling process when using async/await patterns by using the to function from await-to-js to capture errors in a concise and readable manner. This pattern is particularly useful when you want to avoid the verbosity of try/catch blocks in your asynchronous code.
7. Not Everything Can Be Parsed with JSON.parse
Before optimization:
The initial code attempts to parse the data prop using JSON.parse(data) directly. This can be problematic because JSON.parse() expects a valid JSON string and will throw an error if the input isn't a properly formatted JSON string.
const App = (props) => {
const { data } = props;
const dataObj = JSON.parse(data);
}After optimization:
The revised code includes a try/catch block to safely handle the parsing of the data prop. If data is not a valid JSON string, the error is caught and handled, preventing the application from crashing.
const App = (props) => {
const { data } = props;
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('Data is not a valid JSON string');
}
}This optimization ensures that the application remains stable even if it encounters invalid JSON input. By wrapping JSON.parse() in a try/catch block, the code gracefully handles parsing errors and provides a clear message to help with debugging.
8. Modifying Referenced Data Types
Before optimization:
The original code directly mutates each item in the data array by setting the age property to 12. Since data is an array, which is a reference type in JavaScript, these mutations affect the original array passed as a prop.
const App = (props) => {
const { data } = props;
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
});
}
}Users of the App function might be surprised to find that the age values in the data array are always 12 and will not be able to trace where the data was modified. This is because the data array is being mutated directly, which can lead to unexpected side effects outside of the App component.
After optimization:
The updated code creates a deep clone of the data array before making any modifications. This ensures that the original data array is not mutated and prevents potential side effects.
import cloneDeep from 'lodash.clonedeep';
const App = (props) => {
const { data } = props;
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
});
}
}This optimization ensures that the App function does not cause unexpected mutations to the props it receives, by using lodash.clonedeep to create a deep clone before making changes. It is a good practice to avoid direct mutation of reference types to maintain predictable data flow and avoid side effects.
9. Concurrent Asynchronous Assignments
Before optimization:
The original code attempts to fill urlList with URLs fetched asynchronously. However, it erroneously tries to log urlList immediately after initiating the asynchronous operations, which won't work as expected because getUrl is an asynchronous function and data.forEach doesn't wait for it to complete.
const App = (props) => {
const { data } = props;
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}This will likely result in urlList being logged before any of the asynchronous getUrl operations have completed, meaning it will not contain the expected data.
After optimization:
The improved code uses Promise.all to wait for all asynchronous operations to complete before logging urlList. It does this by mapping over data to create an array of promises, then awaiting the resolution of all these promises with Promise.all.
const App = async (props) => {
const { data } = props;
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(item => {
const { id = '' } = item || {};
return getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
await Promise.all(jobs);
console.log(urlList);
}
}By using Promise.all, the code ensures that all asynchronous operations are completed before urlList is logged, which means urlList will contain all the URLs fetched by getUrl. This way, the function behaves as expected, logging the final result of the asynchronous operations.
Alex’s story, while fictional, serves as an important reminder: every mistake is an opportunity to learn. It shows us that continuous improvement is key to becoming a skilled developer. Let Alex’s journey inspire us to turn every error into a stepping stone towards mastery in the art of coding.






