ProgrammingJavaScriptFrontend

The Art of Clean Code: Best Practices for Modern JavaScript

Discover essential principles and practices for writing maintainable, readable JavaScript code that your future self will thank you for.

PW

Piotr Wislowski

12 min read

The Art of Clean Code: Best Practices for Modern JavaScript

Writing clean code is more than just making your program work—it’s about creating code that’s readable, maintainable, and enjoyable to work with. In JavaScript, where flexibility can lead to chaos, following clean code principles becomes even more crucial.

What Makes Code “Clean”?

Clean code should be:

  • Easy to read and understand
  • Self-documenting
  • Consistent in style
  • Simple and focused
  • Easy to modify and extend

As Robert C. Martin said: “Clean code can be read, and enhanced by a developer other than its original author.”

Naming Conventions

Use Descriptive Names

Bad:

const d = new Date();
const u = users.filter(x => x.a);

Good:

const currentDate = new Date();
const activeUsers = users.filter(user => user.isActive);

Be Consistent

Choose a naming convention and stick to it:

// Consistent camelCase
const userName = 'john_doe';
const isUserActive = true;
const getUserData = () => { /* ... */ };

// Consistent for constants
const API_BASE_URL = 'https://api.example.com';
const MAX_RETRY_ATTEMPTS = 3;

Function Design

Keep Functions Small

Functions should do one thing well:

// Bad - doing too many things
function processUser(userData) {
  // Validate data
  if (!userData.email || !userData.name) {
    throw new Error('Invalid user data');
  }

  // Transform data
  const user = {
    id: generateId(),
    email: userData.email.toLowerCase(),
    name: userData.name.trim(),
    createdAt: new Date()
  };

  // Save to database
  database.save(user);

  // Send email
  sendWelcomeEmail(user.email);

  return user;
}

// Good - single responsibility
function validateUserData(userData) {
  if (!userData.email || !userData.name) {
    throw new Error('Invalid user data');
  }
}

function transformUserData(userData) {
  return {
    id: generateId(),
    email: userData.email.toLowerCase(),
    name: userData.name.trim(),
    createdAt: new Date()
  };
}

function createUser(userData) {
  validateUserData(userData);
  const user = transformUserData(userData);

  database.save(user);
  sendWelcomeEmail(user.email);

  return user;
}

Use Pure Functions When Possible

Pure functions are predictable and testable:

// Impure - modifies external state
let total = 0;
function addToTotal(value) {
  total += value;
  return total;
}

// Pure - no side effects
function add(a, b) {
  return a + b;
}

function calculateTotal(values) {
  return values.reduce((sum, value) => sum + value, 0);
}

Error Handling

Use Descriptive Error Messages

// Bad
function divide(a, b) {
  if (b === 0) {
    throw new Error('Error');
  }
  return a / b;
}

// Good
function divide(dividend, divisor) {
  if (divisor === 0) {
    throw new Error(`Cannot divide ${dividend} by zero`);
  }
  return dividend / divisor;
}

Handle Async Errors Properly

// Using async/await with proper error handling
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`Failed to fetch user: ${response.status} ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Error fetching user data:', error);
    throw error; // Re-throw if the caller should handle it
  }
}

Modern JavaScript Features

Use const and let Appropriately

// Bad - var has function scope and can be redeclared
var name = 'John';
var age = 30;

// Good - use const for values that won't change
const userName = 'John';
const apiUrl = 'https://api.example.com';

// Good - use let for values that will change
let currentPage = 1;
let isLoading = false;

Leverage Destructuring

// Bad
function createUserCard(user) {
  const name = user.name;
  const email = user.email;
  const avatar = user.profile.avatar;

  return `<div>${name} - ${email} - ${avatar}</div>`;
}

// Good
function createUserCard({ name, email, profile: { avatar } }) {
  return `<div>${name} - ${email} - ${avatar}</div>`;
}

Use Template Literals

// Bad
const message = 'Hello ' + userName + ', you have ' + messageCount + ' new messages.';

// Good
const message = `Hello ${userName}, you have ${messageCount} new messages.`;

Object and Array Manipulation

Use Array Methods Over Loops

// Bad - imperative style
const activeUsers = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].isActive) {
    activeUsers.push(users[i]);
  }
}

// Good - declarative style
const activeUsers = users.filter(user => user.isActive);

Use Spread Operator for Immutability

// Bad - mutates original object
function updateUser(user, updates) {
  Object.assign(user, updates);
  return user;
}

// Good - returns new object
function updateUser(user, updates) {
  return { ...user, ...updates };
}

Comments and Documentation

Write Self-Documenting Code

// Bad - code needs comments to explain what it does
// Check if user is admin and has permissions
if (user.role === 'admin' && user.permissions.includes('write')) {
  // ...
}

// Good - code explains itself
const isAdminWithWriteAccess = user.role === 'admin' && user.permissions.includes('write');
if (isAdminWithWriteAccess) {
  // ...
}

Comment the “Why”, Not the “What”

// Bad - explains what the code does
// Loop through users array
users.forEach(user => {
  // Send email to user
  sendEmail(user.email);
});

// Good - explains why we're doing something
// Send welcome emails to all newly registered users
// We batch these to avoid overwhelming the email service
const newUsers = users.filter(user => user.isNew);
await sendBatchEmails(newUsers);

Testing and Clean Code

Write testable code by keeping functions pure and dependencies injectable:

// Hard to test - depends on external services
function processPayment(amount) {
  const result = paymentGateway.charge(amount);
  database.saveTransaction(result);
  return result;
}

// Easy to test - dependencies are injected
function processPayment(amount, paymentService, database) {
  const result = paymentService.charge(amount);
  database.saveTransaction(result);
  return result;
}

Code Organization

Use Modules and Proper Imports

// userService.js
export class UserService {
  constructor(apiClient, cache) {
    this.apiClient = apiClient;
    this.cache = cache;
  }

  async getUser(id) {
    const cachedUser = this.cache.get(`user:${id}`);
    if (cachedUser) return cachedUser;

    const user = await this.apiClient.fetchUser(id);
    this.cache.set(`user:${id}`, user);
    return user;
  }
}

// main.js
import { UserService } from './userService.js';
import { ApiClient } from './apiClient.js';
import { Cache } from './cache.js';

const userService = new UserService(
  new ApiClient(),
  new Cache()
);

Conclusion

Clean code isn’t just about following rules—it’s about developing habits that make your code more maintainable, readable, and enjoyable to work with. Start by focusing on:

  1. Meaningful names for variables, functions, and classes
  2. Small, focused functions that do one thing well
  3. Consistent formatting and style
  4. Proper error handling with descriptive messages
  5. Self-documenting code that explains its intent

Remember, you write code once but read it many times. Make it count.

Clean code is a skill that develops over time. Practice these principles consistently, and you’ll find that not only does your code become more maintainable, but you’ll also become a more effective developer.

Your future self (and your teammates) will thank you for the extra effort you put into writing clean, readable code today.