DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

Clean Code Practices in JavaScript: Write Readable, Maintainable Code

Writing JavaScript that works is only the first step. As applications grow, code becomes harder to understand, bugs become more expensive to fix, and even small changes can introduce unexpected problems.

Clean code is about making your intentions obvious. When another developer opens your file six months later, they should be able to understand what the code does without spending hours tracing variables and function calls.

In this article, we'll explore practical clean code principles, including meaningful naming, small functions, early returns, avoiding duplication, immutable updates, and clear error handling. We'll apply these ideas to a realistic JavaScript order-processing example that you can run directly with Node.js.

7 Clean Code Practices Every JavaScript Developer Should Follow

1. Use Meaningful Names

Choose variable and function names that communicate their purpose. A name like calculateOrderTotal explains the intention far better than calc or processData. Clear naming reduces the need for explanatory comments and makes debugging easier.

2. Keep Functions Small and Focused

Each function should have one clear responsibility. Instead of validating input, calculating prices, applying discounts, and printing results in a single large function, separate these operations into smaller functions. Focused functions are easier to test, reuse, and maintain.

3. Prefer Early Returns Over Deep Nesting

Nested conditionals make business logic difficult to follow. Use guard clauses to handle invalid inputs and exceptional cases near the beginning of a function. This keeps the main execution path readable and reduces unnecessary indentation.

4. Avoid Duplicating Logic

When the same calculation or validation appears in multiple places, extract it into a reusable function. This follows the Don't Repeat Yourself (DRY) principle and ensures that future changes can be made in one place without introducing inconsistent behavior.

5. Avoid Unnecessary Mutation

Unexpected changes to shared objects can create difficult-to-reproduce bugs. Prefer returning new objects when updating application data, especially when working with React, Redux, or other state-driven systems. Remember that object spread creates a shallow copy, so nested objects may still be shared.

6. Handle Errors Explicitly

Clean code should define how invalid input and unexpected failures are handled. Validate data at system boundaries, throw meaningful errors when an operation cannot continue, and catch exceptions where the application can recover or provide useful feedback. Avoid silently swallowing errors.

7. Write Code That Is Easy to Test

Functions that receive their dependencies through arguments are generally easier to test than functions that rely on hidden global state. Keep business logic separate from console output, HTTP requests, and database operations whenever practical. This separation improves reliability and makes future refactoring less risky.

The following example combines these practices in a small order-processing workflow. It validates customer orders, calculates item totals, applies a discount, produces a new order summary, and handles invalid input without modifying the original order.

// clean-code-example.js

// Step 1: Define reusable business rules as named constants.
const TAX_RATE = 0.05;
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.10;

// Step 2: Validate an order before performing calculations.
function validateOrder(order) {
  if (!order || typeof order !== 'object') {
    throw new Error('Order must be a valid object.');
  }

  if (!Array.isArray(order.items) || order.items.length === 0) {
    throw new Error('Order must contain at least one item.');
  }

  for (const item of order.items) {
    if (!item.name || typeof item.name !== 'string') {
      throw new Error('Every item must have a valid name.');
    }

    if (!Number.isFinite(item.price) || item.price < 0) {
      throw new Error(`Invalid price for item: ${item.name}`);
    }

    if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
      throw new Error(`Invalid quantity for item: ${item.name}`);
    }
  }

  console.log('Step 1: Order validation completed successfully.');
}

// Step 3: Calculate the total for one item.
function calculateItemTotal(item) {
  return item.price * item.quantity;
}

// Step 4: Calculate the subtotal without modifying the original items.
function calculateSubtotal(items) {
  return items.reduce((subtotal, item) => {
    return subtotal + calculateItemTotal(item);
  }, 0);
}

// Step 5: Apply a discount only when the threshold is reached.
function calculateDiscount(subtotal) {
  if (subtotal < DISCOUNT_THRESHOLD) {
    return 0;
  }

  return subtotal * DISCOUNT_RATE;
}

// Step 6: Calculate tax after applying the discount.
function calculateTax(subtotal, discount) {
  const taxableAmount = subtotal - discount;
  return taxableAmount * TAX_RATE;
}

// Step 7: Build a new summary instead of changing the input order.
function createOrderSummary(order) {
  validateOrder(order);

  const subtotal = calculateSubtotal(order.items);
  console.log(`Step 2: Subtotal calculated: $${subtotal.toFixed(2)}`);

  const discount = calculateDiscount(subtotal);
  console.log(`Step 3: Discount applied: $${discount.toFixed(2)}`);

  const tax = calculateTax(subtotal, discount);
  console.log(`Step 4: Tax calculated: $${tax.toFixed(2)}`);

  const total = subtotal - discount + tax;

  return {
    orderId: order.id,
    customer: order.customer,
    items: order.items.map((item) => ({ ...item })),
    subtotal: Number(subtotal.toFixed(2)),
    discount: Number(discount.toFixed(2)),
    tax: Number(tax.toFixed(2)),
    total: Number(total.toFixed(2)),
  };
}

// Step 8: Keep presentation separate from business calculations.
function printOrderSummary(summary) {
  console.log('\nOrder Summary');
  console.log('--------------');
  console.log(`Order ID: ${summary.orderId}`);
  console.log(`Customer: ${summary.customer}`);
  console.log(`Subtotal: $${summary.subtotal.toFixed(2)}`);
  console.log(`Discount: $${summary.discount.toFixed(2)}`);
  console.log(`Tax: $${summary.tax.toFixed(2)}`);
  console.log(`Total: $${summary.total.toFixed(2)}`);
}

// Step 9: Create sample data for a realistic order.
const order = {
  id: 'ORD-1001',
  customer: 'Alex Johnson',
  items: [
    { name: 'Keyboard', price: 65, quantity: 1 },
    { name: 'Mouse', price: 25, quantity: 2 },
  ],
};

// Step 10: Run the workflow and handle failures explicitly.
try {
  console.log('Starting order processing...');

  const summary = createOrderSummary(order);
  printOrderSummary(summary);

  console.log('\nStep 5: Order processing completed successfully.');
  console.log('Original order remains unchanged:', order.id);
} catch (error) {
  console.error('Order processing failed:', error.message);
}

// Step 11: Demonstrate how invalid data is handled.
const invalidOrder = {
  id: 'ORD-1002',
  customer: 'Taylor Smith',
  items: [{ name: 'Monitor', price: -50, quantity: 1 }],
};

try {
  console.log('\nTesting invalid order data...');
  createOrderSummary(invalidOrder);
} catch (error) {
  console.error('Expected validation error:', error.message);
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Clean code is not about writing the fewest lines possible or following arbitrary formatting rules. It is about reducing cognitive load, making business logic easy to understand, and helping developers change software confidently.

Start with a few practical improvements: choose descriptive names, extract focused functions, use guard clauses, remove duplicated logic, and handle errors deliberately. These small changes can make even an existing codebase significantly easier to maintain.

Remember that clean code is a team practice, not a one-time refactoring task. Combine consistent conventions with code reviews, automated tests, and a linter such as ESLint to keep quality from declining as your project grows.

Which clean code practice has made the biggest difference in your JavaScript projects: meaningful naming, smaller functions, or avoiding unnecessary mutation?

Top comments (0)