Frontend Development 10 min read

Common JavaScript Pitfalls and Best Practices for Frontend Development

This article examines frequent JavaScript errors such as improper data handling, unsafe destructuring, unreliable default values, misuse of array methods, object method calls, async/await error handling, JSON parsing, mutable references, concurrent async operations, and over‑defensive coding, and provides concise ES6‑compatible refactorings to prevent white‑screen crashes and improve robustness.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Common JavaScript Pitfalls and Best Practices for Frontend Development

In many frontend projects, developers encounter white‑screen errors caused by unexpected data formats returned from the server, such as receiving null instead of an empty array [] , which leads to failures when calling methods like forEach . Proper validation and defensive coding are required to avoid these crashes.

When a component receives a data prop that is undefined or null , destructuring it directly causes runtime errors. The safe pattern is to provide a fallback object, e.g., const { name, age } = data || {}; , ensuring the destructuring never operates on a non‑object.

const App = (props) => {
  const { data } = props;
  const { name, age } = data || {};
}

Default parameter values in ES6 are only applied when the argument is strictly undefined . Therefore, using props = {} and const { data = {} } = props does not protect against null values, and the code will still throw if data is null . A strict equality check or fallback is necessary.

const App = (props = {}) => {
  const { data = {} } = props;
  const { name, age } = data;
}

Array methods such as map must be called on real arrays. When data is a number or any non‑array value, data || [] still yields the original non‑array, causing map to fail. The reliable guard is Array.isArray(data) before invoking array methods.

const App = (props) => {
  const { data } = props;
  let nameList = [];
  if (Array.isArray(data)) {
    nameList = data.map(item => item.name);
  }
}

When iterating over an array whose items may be undefined or null , accessing properties like item.name will throw. Using optional chaining ( item?.name ) prevents the error but adds compiled code size; it should be used judiciously.

const App = (props) => {
  const { data } = props;
  let infoList = [];
  if (Array.isArray(data)) {
    infoList = data.map(item => `My name is ${item?.name}, I am ${item?.age} years old`);
  }
}

Object methods like Object.keys require a genuine object. Passing undefined or null leads to errors. A safe approach is Object.keys(data || {}) or a custom isPlainObject check.

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);
  }
}

Async/await calls should be wrapped in try/catch to handle rejected promises; otherwise the UI may stay in a loading state. Libraries like await-to-js provide a cleaner error‑handling pattern.

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());
    setLoading(false);
    // handle err or use res
  };
}

Parsing JSON should be guarded with try/catch rather than pre‑checking the string, because JSON.parse throws on invalid input.

const App = (props) => {
  const { data } = props;
  let dataObj = {};
  try {
    dataObj = JSON.parse(data);
  } catch (error) {
    console.error('data is not a valid JSON string');
  }
}

Mutating reference‑type arguments inside a function can unintentionally affect callers. Using a deep clone (e.g., lodash.clonedeep ) isolates the original data.

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;
    });
  }
}

When performing concurrent asynchronous operations that populate an array, the synchronous forEach loop finishes before the promises resolve, leading to incomplete results. Collect the promises in an array and await Promise.all before using the final list.

const App = async (props) => {
  const { data } = props;
  let urlList = [];
  if (Array.isArray(data)) {
    const jobs = data.map(async item => {
      const { id = '' } = item || {};
      const res = await getUrl(id);
      if (res) urlList.push(res);
      return res;
    });
    await Promise.all(jobs);
    console.log(urlList);
  }
}

Over‑defensive code such as using optional chaining after a guaranteed array map result adds unnecessary complexity. Directly calling infoList.join(',') is sufficient.

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}, I am ${age} years old`;
    });
  }
  const info = infoList.join(',');
}

All the issues discussed are fundamental JavaScript concepts unrelated to any specific framework; mastering them is essential for reliable frontend development.

frontendJavaScriptbest practiceserror handlingES6
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.