preloader
If you want to request blogpost topics, feel free to inbox my instagram @adabjawa. Thankyou!
JavaScript Best Practices for Writing Clean Code

JavaScript Best Practices for Writing Clean Code

In the fast-paced world of web development, writing clean and maintainable code is more important than ever. Whether you’re working on a personal project, a client application, or contributing to a large-scale team project, clean code ensures that your work is easy to understand, scalable, and adaptable to future changes.

JavaScript, as one of the most popular and versatile programming languages, is at the heart of modern web applications. However, its flexibility can sometimes lead to messy, hard-to-read code if not managed properly. That’s why following JavaScript best practices is essential for creating high-quality applications, whether you’re building with React.js, Next.js, Express.js, or plain JavaScript.

This guide will walk you through the core principles of writing clean, effective, and maintainable JavaScript code. By incorporating these best practices into your daily development routine, you’ll not only improve the readability and performance of your applications but also make life easier for you and your teammates.

1. Use const and let Instead of var

One of the simplest ways to improve your JavaScript code is to stop using var. Instead, use const and let which provide block scope and prevent issues related to hoisting and reassignment.

Example:

// Bad (Using var)
var name = "John";
var age = 30;

// Good (Using const and let)
const name = "John"; // Constant value that won’t change
let age = 30; // Variable value that may change later

Why?

  • const prevents reassignment, making your code more predictable.
  • let allows block scoping, reducing the chance of accidental redeclaration.
  • var can lead to confusing bugs due to hoisting and function scoping.

2. Write Descriptive Variable and Function Names

Avoid using single-letter or ambiguous names for variables and functions. Use names that describe the purpose or the value that they hold.

Example:

// Bad
const n = 5;
function a() {
  return n * n;
}

// Good
const numberOfItems = 5;
function calculateSquare(number) {
  return number * number;
}

Why?

Descriptive names make your code more readable and maintainable. Other developers (or future you) will immediately understand what the variable or function does.


3. Keep Functions Small and Focused

Functions should perform a single task and be as short as possible. If a function is doing too many things, break it down into smaller functions.

Example:

// Bad (Too much responsibility)
function processUser(user) {
  validateUser(user);
  saveUserToDatabase(user);
  sendWelcomeEmail(user);
}

// Good (Separation of concerns)
function processUser(user) {
  validateUser(user);
  saveUser(user);
  sendWelcomeEmail(user);
}

function validateUser(user) {
  // validation logic
}

function saveUser(user) {
  // save user to database
}

function sendWelcomeEmail(user) {
  // email logic
}

Why?

Small, focused functions are easier to test, debug, and reuse. This approach follows the Single Responsibility Principle (SRP).


4. Avoid Repetitive Code (DRY Principle)

The Don’t Repeat Yourself (DRY) principle is fundamental to writing clean code. Avoid duplicating logic by abstracting it into functions or reusable components.

Example:

// Bad (Repetitive code)
const taxRate = 0.2;
const priceWithTax = price * (1 + taxRate);
const salaryWithTax = salary * (1 + taxRate);

// Good (DRY)
const taxRate = 0.2;

function applyTax(amount) {
  return amount * (1 + taxRate);
}

const priceWithTax = applyTax(price);
const salaryWithTax = applyTax(salary);

Why?

Avoiding repetition makes your code more maintainable and less prone to bugs. Changes only need to be made in one place.


5. Use Arrow Functions Where Appropriate

Arrow functions provide a shorter syntax and do not bind their own this context, which makes them especially useful in callbacks.

Example:

// Bad (Function expression)
function add(a, b) {
  return a + b;
}

// Good (Arrow function)
const add = (a, b) => a + b;

Why?

Arrow functions are more concise and allow for cleaner handling of the this keyword in certain contexts, especially in React components or event handlers.


6. Use Template Literals for String Concatenation

Instead of using string concatenation with +, use template literals for clearer, more readable string interpolation.

Example:

// Bad
const message = 'Hello, ' + name + '! You have ' + notifications + ' notifications.';

// Good
const message = `Hello, ${name}! You have ${notifications} notifications.`;

Why?

Template literals make string handling easier to read and maintain, especially when dealing with multiple variables or expressions inside strings.


7. Handle Errors Gracefully with try/catch

When working with asynchronous code or potential failures (like network requests), ensure you handle errors properly using try/catch blocks or .catch() for Promises.

Example:

// Bad (No error handling)
async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

// Good (With error handling)
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Failed to fetch');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
    return null;
  }
}

Why?

Error handling prevents your application from crashing and provides better user experience by handling failures gracefully.


8. Use Default Function Parameters

When writing functions, set default values for parameters to make your code more robust and prevent potential issues with undefined values.

Example:

// Bad
function greet(name) {
  return `Hello, ${name || 'Guest'}!`;
}

// Good
function greet(name = 'Guest') {
  return `Hello, ${name}!`;
}

Why?

Default parameters simplify code by providing fallbacks and reducing the need for additional checks.


9. Use Promises and async/await for Asynchronous Code

When dealing with asynchronous operations, use Promises and the async/await syntax for cleaner and more readable code.

Example:

// Bad (Using callbacks)
function fetchData(callback) {
  setTimeout(() => {
    callback(null, { data: 'Sample Data' });
  }, 1000);
}

// Good (Using Promises with async/await)
async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ data: 'Sample Data' });
    }, 1000);
  });
}

Why?

Promises and async/await make asynchronous code easier to read and maintain, eliminating the “callback hell.”


10. Avoid Mutating Data (Immutability)

When working with objects or arrays, avoid mutating the original data. Instead, create copies of data to prevent side effects, especially in functional programming and React development.

Example:

// Bad (Mutating an array)
const numbers = [1, 2, 3];
numbers.push(4);

// Good (Using immutable methods)
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4];

Why?

Immutability makes your code more predictable, easier to debug, and safer in a concurrent environment.


Here are a few more JavaScript best practices to expand on the original list:


11. Avoid Deep Nesting

Deeply nested code can become difficult to follow and maintain. Try to reduce the levels of nesting by refactoring your code, using early returns, or separating logic into smaller functions.

Example:

// Bad (Deep nesting)
function processUser(user) {
  if (user) {
    if (user.isActive) {
      if (user.hasPaid) {
        return 'User is active and has paid';
      }
    }
  }
  return 'Invalid user';
}

// Good (Using early returns)
function processUser(user) {
  if (!user || !user.isActive || !user.hasPaid) {
    return 'Invalid user';
  }
  return 'User is active and has paid';
}

Why?

Reducing nesting makes code more readable, easier to follow, and reduces cognitive load.


12. Use Object Destructuring for Cleaner Code

Object destructuring allows you to extract properties from objects and arrays in a more concise and readable way.

Example:

// Bad
const user = { name: 'John', age: 30 };
const name = user.name;
const age = user.age;

// Good
const { name, age } = user;

Why?

Destructuring makes your code more concise and removes repetitive access to object properties.


13. Use Array Methods Instead of Loops

Instead of using traditional for loops, prefer higher-order functions like map(), filter(), reduce(), and forEach() for working with arrays. These methods are more declarative and expressive.

Example:

// Bad (Using for loop)
const numbers = [1, 2, 3, 4];
const doubleNumbers = [];
for (let i = 0; i < numbers.length; i++) {
  doubleNumbers.push(numbers[i] * 2);
}

// Good (Using map)
const numbers = [1, 2, 3, 4];
const doubleNumbers = numbers.map(number => number * 2);

Why?

Array methods like map and filter result in cleaner, more readable code. They also emphasize immutability, since they return new arrays instead of modifying the original one.


14. Use Strict Equality (===)

Always use === (strict equality) instead of == (loose equality) to avoid unexpected type coercion.

Example:

// Bad
if (1 == '1') {
  console.log('Equal'); // true, but type coercion happens
}

// Good
if (1 === '1') {
  console.log('Equal'); // false, since types are not the same
}

Why?

Strict equality avoids implicit type conversion, reducing bugs caused by comparing different data types.


15. Comment Your Code When Necessary

While clean code is often self-explanatory, sometimes comments are necessary to explain the why behind complex logic or certain decisions.

Example:

// Bad (No comments for complex logic)
const calculateDiscount = (price) => price > 100 ? price * 0.9 : price;

// Good (Commenting the logic)
const calculateDiscount = (price) => {
  // If the price is above 100, apply a 10% discount
  return price > 100 ? price * 0.9 : price;
};

Why?

Comments can explain the reasoning behind decisions or clarify complex logic. However, avoid over-commenting or explaining obvious code.


16. Use Constants for Magic Numbers

Avoid hardcoding numbers (magic numbers) directly in your code. Instead, store them in constants with descriptive names.

Example:

// Bad (Magic number)
function calculateDiscount(price) {
  return price * 0.07; // What is 0.07?
}

// Good (Using a named constant)
const TAX_RATE = 0.07;

function calculateDiscount(price) {
  return price * TAX_RATE;
}

Why?

Using named constants makes your code more readable and easier to maintain, especially when numbers have specific meanings or are used in multiple places.


17. Use null or undefined Appropriately

Be mindful of when to use null and undefined. Use null to explicitly define an “empty” value, while undefined indicates the absence of a value (e.g., uninitialized variables).

Example:

// Bad
let name = undefined; // It's better to use null for explicitly empty values

// Good
let name = null; // Indicates an intentional empty value

Why?

Understanding the difference between null and undefined helps avoid unintended bugs and makes code more semantically meaningful.


18. Refactor Long switch Statements

Avoid using long switch statements when there are cleaner alternatives like objects or dictionaries for lookup.

Example:

// Bad (Using switch)
function getRole(role) {
  switch (role) {
    case 'admin':
      return 'Administrator';
    case 'user':
      return 'Regular User';
    case 'guest':
      return 'Guest';
    default:
      return 'Unknown Role';
  }
}

// Good (Using object lookup)
const roles = {
  admin: 'Administrator',
  user: 'Regular User',
  guest: 'Guest',
};

function getRole(role) {
  return roles[role] || 'Unknown Role';
}

Why?

Object lookups are more readable and easier to maintain than long switch statements.


19. Avoid Side Effects in Functions

A function should, whenever possible, avoid mutating variables outside of its scope (side effects). Pure functions that only rely on their inputs are easier to test and debug.

Example:

// Bad (Side effect)
let counter = 0;
function increment() {
  counter++;
}

// Good (Pure function)
function increment(counter) {
  return counter + 1;
}

Why?

Pure functions are predictable and less prone to introducing bugs, making them a cornerstone of functional programming.


20. Use ESLint and Prettier

To ensure consistent code formatting and catch potential issues early, use tools like ESLint and Prettier. These tools can be integrated into your workflow to enforce clean code standards automatically.

Example:

  • ESLint: Helps identify and fix common code issues and enforces coding standards.
  • Prettier: Automatically formats code for consistency and readability.

Why?

These tools help maintain code quality, consistency, and prevent potential bugs early in the development process.


Here are a few more advanced JavaScript best practices that can further improve your clean coding approach:


21. Use async/await Over then() for Promises

Using async/await improves readability and simplifies chaining of promises. It also helps avoid callback hell.

Example:

// Bad (Using .then)
fetchUserData()
  .then((user) => fetchUserPosts(user.id))
  .then((posts) => displayPosts(posts))
  .catch((error) => handleError(error));

// Good (Using async/await)
async function fetchAndDisplayUserData() {
  try {
    const user = await fetchUserData();
    const posts = await fetchUserPosts(user.id);
    displayPosts(posts);
  } catch (error) {
    handleError(error);
  }
}

Why?

async/await is more readable and makes error handling with try/catch clearer compared to using .then() and .catch() for chaining promises.


22. Avoid Modifying Function Parameters

Changing function parameters within a function can introduce bugs, especially when passing objects or arrays by reference. Always treat function parameters as immutable and return new values instead of modifying the original ones.

Example:

// Bad (Modifying parameter)
function updateUser(user) {
  user.isAdmin = true;
  return user;
}

// Good (Creating a new object)
function updateUser(user) {
  return { ...user, isAdmin: true };
}

Why?

Keeping function parameters immutable prevents unexpected side effects, especially when working with shared data in a complex application.


23. Avoid Using the new Keyword for Object Creation

Instead of using the new keyword to create objects, consider using factory functions or classes to handle object instantiation. This approach is cleaner, avoids issues with this context, and is more predictable.

Example:

// Bad (Using new with function constructor)
function Person(name) {
  this.name = name;
}
const person = new Person('John');

// Good (Using class or factory function)
class Person {
  constructor(name) {
    this.name = name;
  }
}

const person = new Person('John');

Why?

Using classes or factory functions makes your code more consistent and easier to work with, especially for object creation and inheritance.


24. Use Functional Programming Principles

Incorporate functional programming principles like pure functions, higher-order functions, and immutability. Avoid mutating state directly and use methods like map, filter, and reduce to process data without side effects.

Example:

// Bad (Imperative, mutating array)
const numbers = [1, 2, 3, 4];
for (let i = 0; i < numbers.length; i++) {
  numbers[i] *= 2;
}

// Good (Declarative, using map)
const numbers = [1, 2, 3, 4];
const doubled = numbers.map((n) => n * 2);

Why?

Functional programming leads to more predictable, testable, and maintainable code by emphasizing immutability and reducing side effects.


25. Use Optional Chaining and Nullish Coalescing

Optional chaining (?.) allows you to safely access deeply nested properties, avoiding runtime errors when a property doesn’t exist. Nullish coalescing (??) provides a default value only when null or undefined is encountered.

Example:

// Bad
const user = {};
const city = user && user.address && user.address.city;

// Good (Using optional chaining and nullish coalescing)
const user = {};
const city = user?.address?.city ?? 'Unknown';

Why?

Optional chaining and nullish coalescing simplify your code and prevent runtime errors when accessing potentially null or undefined values.


26. Use for...of and for...in Instead of forEach When Necessary

Although .forEach() is a common way to iterate through arrays, for...of provides greater flexibility, such as allowing the use of break or continue statements.

Example:

// Bad (Using forEach without control flow)
[1, 2, 3, 4].forEach((num) => {
  if (num === 3) {
    return; // This doesn't break out of the loop, just skips the current iteration
  }
  console.log(num);
});

// Good (Using for...of with break/continue)
for (const num of [1, 2, 3, 4]) {
  if (num === 3) continue; // Skips this iteration
  console.log(num);
}

Why?

for...of allows greater control in loops with features like break and continue, which cannot be used with .forEach().


27. Prefer Named Exports Over Default Exports

Using named exports makes it easier to identify and import exactly what you need from a module. It also helps avoid issues with default exports when refactoring code.

Example:

// Bad (Using default export)
export default function calculateTotal() {
  //...
}

// Good (Using named export)
export function calculateTotal() {
  //...
}

Why?

Named exports provide better auto-completion in editors and prevent name collisions, making it clearer which module you are importing.


28. Avoid Using eval()

Using eval() can be dangerous as it executes arbitrary code, making your code vulnerable to attacks like code injection. Avoid it in all circumstances.

Example:

// Bad (Using eval)
const code = 'console.log("Hello World")';
eval(code); // This is dangerous and prone to security issues

// Good (Avoid eval)
const code = 'console.log("Hello World")';
// Manually execute or refactor the code without eval

Why?

eval() can introduce security vulnerabilities and lead to unpredictable behavior. Modern JavaScript rarely, if ever, requires eval() to solve a problem.


29. Use .bind() or Arrow Functions to Preserve this in Callbacks

In event handlers or callbacks, this can refer to different contexts. Use .bind() or arrow functions to explicitly bind this to the appropriate context.

Example:

// Bad (Incorrect use of this in callback)
class MyComponent {
  handleClick() {
    console.log(this); // `this` will not refer to the class instance
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

// Good (Using arrow function or bind to preserve this)
class MyComponent {
  handleClick = () => {
    console.log(this); // `this` refers to the class instance
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

Why?

Using .bind() or arrow functions ensures that this refers to the correct context, avoiding potential bugs in event handlers or callbacks.


30. Use Immutable Data Structures When Possible

In certain contexts (e.g., React or Redux), using immutable data structures like Immutable.js or native JavaScript methods that don’t mutate data (map, filter, reduce) helps prevent unintended side effects and ensures data consistency.

Example:

// Bad (Mutating original array)
const numbers = [1, 2, 3];
numbers.push(4);

// Good (Using immutable approach)
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4];

Why?

Immutability ensures data is not inadvertently modified in ways that affect other parts of the application, leading to more predictable and reliable code.


Conclusion

By applying these advanced best practices in your JavaScript code, you’ll be able to write clean, efficient, and maintainable applications, whether in React.js, Node.js, or any other environment. These techniques help streamline the development process, improve code readability, and reduce the chances of bugs and issues down the line.


comments powered by Disqus

Related Posts

The Most Effective and Simple Way to Implement Redux Toolkit Query in Next.js with CRUD Operations

The Most Effective and Simple Way to Implement Redux Toolkit Query in Next.js with CRUD Operations

When building modern web applications, efficiency and simplicity are crucial. Redux Toolkit Query (RTK Query) simplifies data fetching and caching, making it an excellent choice for managing server state.

Read More
Text Classification Using the Naive Bayes Algorithm in JavaScript

Text Classification Using the Naive Bayes Algorithm in JavaScript

Introduction: In this article, we will explore the Naive Bayes algorithm for text classification and how to implement it using JavaScript ES6 classes.

Read More
Atomic Design with Next.js: A Step-by-Step Guide

Atomic Design with Next.js: A Step-by-Step Guide

Introduction: What is Atomic Design? Atomic Design is a methodology for creating design systems with a clear, hierarchical structure.

Read More