DEV Community

Cover image for JavaScript Clean Code Mastery: Part 2 - Functions That Do One Thing Well
sizan mahmud0
sizan mahmud0

Posted on

JavaScript Clean Code Mastery: Part 2 - Functions That Do One Thing Well

Welcome Back, Code Cleaners!

In Part 1, we mastered meaningful variable names and killed the var keyword forever. Today, we're tackling the heart of clean code: functions.

I once wrote a 250-line function called processUserData(). It validated, transformed, saved to database, sent emails, logged activities, and made coffee (okay, not that last one). When a bug appeared, I spent 4 hours finding it in that monster function.

Never again.

Today's Mission:

  • Write small functions that do ONE thing
  • Master arrow functions (and avoid common mistakes)
  • Name functions like a pro
  • Make your code self-documenting

Let's transform those god functions into clean, testable masterpieces.


Practice 1: Write Small Functions That Do One Thing

The Golden Rule: If you can't describe what a function does in one sentence without using "and", it's doing too much.

❌ Bad: God Function

function processUserData(user) {
  // Validate
  if (!user.email.includes('@')) {
    throw new Error('Invalid email');
  }
  if (user.age < 18) {
    throw new Error('User too young');
  }

  // Transform
  user.name = user.name.trim().toLowerCase();
  user.email = user.email.toLowerCase();

  // Save to database
  const query = `INSERT INTO users (name, email, age) VALUES ('${user.name}', '${user.email}', ${user.age})`;
  db.execute(query);

  // Send welcome email
  const subject = 'Welcome!';
  const body = `Hello ${user.name}, welcome to our platform!`;
  emailService.send(user.email, subject, body);

  // Log activity
  logger.info(`User ${user.name} registered at ${new Date()}`);

  return user;
}
Enter fullscreen mode Exit fullscreen mode

What's wrong?

  • Does 5 different things (validate, transform, save, email, log)
  • Impossible to test one piece without triggering all others
  • Can't reuse validation elsewhere
  • 25 lines of tangled logic

✅ Good: Single Responsibility Functions

function validateUserEmail(email) {
  if (!email.includes('@')) {
    throw new Error('Invalid email');
  }
}

function validateUserAge(age) {
  if (age < 18) {
    throw new Error('User must be 18 or older');
  }
}

function normalizeUserData(user) {
  return {
    ...user,
    name: user.name.trim().toLowerCase(),
    email: user.email.toLowerCase()
  };
}

function saveUserToDatabase(user) {
  const query = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
  return db.execute(query, [user.name, user.email, user.age]);
}

function sendWelcomeEmail(user) {
  const subject = 'Welcome!';
  const body = `Hello ${user.name}, welcome to our platform!`;
  return emailService.send(user.email, subject, body);
}

function logUserRegistration(user) {
  logger.info(`User ${user.name} registered at ${new Date()}`);
}

// Compose small functions into a workflow
async function registerUser(user) {
  validateUserEmail(user.email);
  validateUserAge(user.age);

  const normalizedUser = normalizeUserData(user);

  await saveUserToDatabase(normalizedUser);
  await sendWelcomeEmail(normalizedUser);

  logUserRegistration(normalizedUser);

  return normalizedUser;
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • ✅ Each function is 3-5 lines (easy to understand)
  • ✅ Easy to test (test validateUserEmail independently)
  • ✅ Reusable (use validateUserEmail in 10 different places)
  • ✅ Easy to modify (change email logic without touching registration)
  • ✅ Names explain what happens (no comments needed!)

The 20-Line Rule: If your function exceeds 20 lines, it's probably doing too much. Break it down.


Practice 2: Function Names Should Be Verbs

Functions DO things. Names should reflect actions.

❌ Bad: Noun-Based Names

function user(id) {
  return db.query('SELECT * FROM users WHERE id = ?', [id]);
}

function email(user, subject, body) {
  emailService.send(user.email, subject, body);
}

function validation(data) {
  return data.age > 18;
}
Enter fullscreen mode Exit fullscreen mode

✅ Good: Verb-Based Names

function getUser(id) {
  return db.query('SELECT * FROM users WHERE id = ?', [id]);
}

function sendEmail(user, subject, body) {
  emailService.send(user.email, subject, body);
}

function validateAge(age) {
  return age >= 18;
}
Enter fullscreen mode Exit fullscreen mode

Common Verb Prefixes:

  • get - Retrieve data: getUser(), getOrders()
  • set - Modify data: setUsername(), setTheme()
  • create - Make new: createUser(), createOrder()
  • update - Change existing: updateProfile(), updateStatus()
  • delete - Remove: deleteAccount(), deletePost()
  • validate - Check validity: validateEmail(), validatePassword()
  • calculate - Compute: calculateTotal(), calculateTax()
  • format - Transform: formatDate(), formatCurrency()
  • is/has/can - Return boolean: isValid(), hasPermission(), canEdit()

Practice 3: Use Arrow Functions (But Know When NOT To)

Arrow functions are clean, concise, and solve the dreaded this binding issue. But they're not always the right choice.

✅ Good: When to Use Arrow Functions

1. Array Methods

const numbers = [1, 2, 3, 4, 5];

// ❌ Traditional function - verbose
const doubled = numbers.map(function(number) {
  return number * 2;
});

// ✅ Arrow function - clean
const doubled = numbers.map(number => number * 2);

// ✅ Multiple operations
const result = numbers
  .filter(num => num > 2)
  .map(num => num * 2)
  .reduce((sum, num) => sum + num, 0);
Enter fullscreen mode Exit fullscreen mode

2. Callbacks

// ❌ Traditional function
setTimeout(function() {
  console.log('Delayed');
}, 1000);

// ✅ Arrow function
setTimeout(() => {
  console.log('Delayed');
}, 1000);
Enter fullscreen mode Exit fullscreen mode

3. Promise Chains

// ✅ Clean promise chain
fetchUser(userId)
  .then(user => fetchOrders(user.id))
  .then(orders => processOrders(orders))
  .catch(error => handleError(error));
Enter fullscreen mode Exit fullscreen mode

4. Lexical this Binding

// ❌ Traditional function - 'this' is undefined
const person = {
  name: 'Alice',
  greet: function() {
    setTimeout(function() {
      console.log('Hello, ' + this.name);  // this is undefined!
    }, 1000);
  }
};

// ✅ Arrow function - inherits 'this'
const person = {
  name: 'Alice',
  greet: function() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`);  // works!
    }, 1000);
  }
};
Enter fullscreen mode Exit fullscreen mode

❌ Bad: When NOT to Use Arrow Functions

1. Object Methods

// ❌ Arrow function as method - 'this' won't work
const user = {
  name: 'Alice',
  greet: () => {
    console.log(`Hello, ${this.name}`);  // this is undefined!
  }
};

// ✅ Regular function
const user = {
  name: 'Alice',
  greet() {
    console.log(`Hello, ${this.name}`);  // works!
  }
};
Enter fullscreen mode Exit fullscreen mode

2. Constructors

// ❌ Can't use arrow functions as constructors
const Person = (name) => {
  this.name = name;
};
const alice = new Person('Alice');  // TypeError!

// ✅ Regular function or class
class Person {
  constructor(name) {
    this.name = name;
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Methods That Need arguments

// ❌ Arrow functions don't have 'arguments'
const sum = () => {
  return Array.from(arguments).reduce((a, b) => a + b);  // ReferenceError!
};

// ✅ Regular function or rest parameters
function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}
Enter fullscreen mode Exit fullscreen mode

Practice 4: Keep Function Parameters to 3 or Fewer

The Problem: Too many parameters are hard to remember and error-prone.

❌ Bad: Many Parameters

function createUser(name, email, age, address, phone, country, city, zipCode) {
  // Which order again? Easy to mess up!
  return { name, email, age, address, phone, country, city, zipCode };
}

// Caller has to remember exact order
createUser('Alice', 'alice@example.com', 25, '123 Main St', '555-0100', 'USA', 'NYC', '10001');
Enter fullscreen mode Exit fullscreen mode

✅ Good: Use Object Parameter

function createUser({ name, email, age, address, phone, country, city, zipCode }) {
  return { name, email, age, address, phone, country, city, zipCode };
}

// Caller uses named properties - order doesn't matter!
createUser({
  name: 'Alice',
  email: 'alice@example.com',
  age: 25,
  city: 'NYC',
  country: 'USA',
  address: '123 Main St',
  phone: '555-0100',
  zipCode: '10001'
});

// Even better: With defaults
function createUser({ 
  name, 
  email, 
  age = 18,  // Default age
  country = 'USA'  // Default country
}) {
  return { name, email, age, country };
}

createUser({ name: 'Alice', email: 'alice@example.com' });
// Returns: { name: 'Alice', email: 'alice@example.com', age: 18, country: 'USA' }
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Self-documenting (clear what each parameter is)
  • Order-independent (can't mess up parameter order)
  • Easy to add optional parameters
  • Can provide defaults easily

Practice 5: Don't Use Flags as Function Parameters

The Problem: Boolean flags indicate a function doing multiple things.

❌ Bad: Boolean Flags

function createUser(name, email, isAdmin) {
  const user = { name, email };

  if (isAdmin) {
    user.role = 'admin';
    user.permissions = ['read', 'write', 'delete'];
    sendAdminWelcomeEmail(user);
  } else {
    user.role = 'user';
    user.permissions = ['read'];
    sendUserWelcomeEmail(user);
  }

  return user;
}

// What does 'true' mean here? Have to check function definition!
createUser('Alice', 'alice@example.com', true);
Enter fullscreen mode Exit fullscreen mode

✅ Good: Separate Functions

function createRegularUser(name, email) {
  const user = {
    name,
    email,
    role: 'user',
    permissions: ['read']
  };

  sendUserWelcomeEmail(user);
  return user;
}

function createAdminUser(name, email) {
  const user = {
    name,
    email,
    role: 'admin',
    permissions: ['read', 'write', 'delete']
  };

  sendAdminWelcomeEmail(user);
  return user;
}

// Clear what's happening!
createRegularUser('Alice', 'alice@example.com');
createAdminUser('Bob', 'bob@example.com');
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Refactoring a Checkout Function

Before: Messy

function checkout(cart, user, paymentMethod, shippingAddress, billingAddress, coupon) {
  // Validate cart
  if (cart.items.length === 0) throw new Error('Cart is empty');

  // Calculate totals
  let subtotal = 0;
  for (let i = 0; i < cart.items.length; i++) {
    subtotal += cart.items[i].price * cart.items[i].quantity;
  }

  // Apply coupon
  let discount = 0;
  if (coupon) {
    discount = coupon.type === 'percent' ? subtotal * (coupon.value / 100) : coupon.value;
  }

  // Calculate tax
  const tax = (subtotal - discount) * 0.08;

  // Calculate shipping
  let shipping = subtotal > 100 ? 0 : 10;

  const total = subtotal - discount + tax + shipping;

  // Process payment
  const payment = processPayment(paymentMethod, total);
  if (!payment.success) throw new Error('Payment failed');

  // Create order
  const order = {
    user: user.id,
    items: cart.items,
    subtotal,
    discount,
    tax,
    shipping,
    total,
    shippingAddress,
    billingAddress
  };

  // Save and send email
  saveOrder(order);
  sendOrderConfirmation(user.email, order);

  return order;
}
Enter fullscreen mode Exit fullscreen mode

After: Clean

function calculateSubtotal(items) {
  return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}

function calculateDiscount(subtotal, coupon) {
  if (!coupon) return 0;

  return coupon.type === 'percent' 
    ? subtotal * (coupon.value / 100) 
    : coupon.value;
}

function calculateTax(amount, taxRate = 0.08) {
  return amount * taxRate;
}

function calculateShipping(subtotal, freeShippingThreshold = 100) {
  return subtotal >= freeShippingThreshold ? 0 : 10;
}

function validateCart(cart) {
  if (cart.items.length === 0) {
    throw new Error('Cart is empty');
  }
}

async function checkout({ cart, user, paymentMethod, shippingAddress, billingAddress, coupon }) {
  // Validate
  validateCart(cart);

  // Calculate amounts
  const subtotal = calculateSubtotal(cart.items);
  const discount = calculateDiscount(subtotal, coupon);
  const taxableAmount = subtotal - discount;
  const tax = calculateTax(taxableAmount);
  const shipping = calculateShipping(subtotal);
  const total = taxableAmount + tax + shipping;

  // Process payment
  const payment = await processPayment(paymentMethod, total);
  if (!payment.success) {
    throw new Error('Payment failed');
  }

  // Create and save order
  const order = {
    userId: user.id,
    items: cart.items,
    amounts: { subtotal, discount, tax, shipping, total },
    shippingAddress,
    billingAddress
  };

  await saveOrder(order);
  await sendOrderConfirmation(user.email, order);

  return order;
}
Enter fullscreen mode Exit fullscreen mode

Improvements:

  • ✅ Each calculation is a separate function (testable!)
  • ✅ Used object parameter (clear what's passed)
  • ✅ Function names explain what they do
  • ✅ Main function reads like a story
  • ✅ Easy to modify any calculation independently

Quick Wins Checklist for Part 2

Audit your functions with these questions:

Can I describe each function in one sentence without "and"?
Is each function under 20 lines?
Do function names start with verbs?
Am I using arrow functions for callbacks/array methods?
Do I have more than 3 function parameters? (Use object instead)
Do I have boolean flags as parameters? (Split into separate functions)


Part 2 Conclusion: Functions Are the Building Blocks

Great functions are like LEGO blocks:

  • Small and focused
  • Easy to understand
  • Can be combined in infinite ways
  • Reusable everywhere

When you write a 250-line god function, you're building with concrete blocks. Heavy, inflexible, hard to modify.

When you write 10-line focused functions, you're building with LEGOs. Light, flexible, infinitely reconfigurable.

Challenge for Today: Find your longest function. Break it into 3-5 smaller functions. Share the before/after line count in the comments! 📊


Coming Up in Part 3: Modern JavaScript Features 🚀

Next time, we'll explore the modern JavaScript features that make your code cleaner and more expressive:

  • Destructuring - Unpack objects and arrays cleanly
  • Template Literals - Say goodbye to string concatenation hell
  • Optional Chaining - Stop defensive null checking
  • Nullish Coalescing - Better default values
  • Spread Operator - Copy and merge like a pro

These features will cut your code by 30-40% while making it more readable. Don't miss it!


Ready to write better functions? 👏 Clap for clean functions! (50 claps available)

Get notified of Part 3! 🔔 Follow me - Part 3 drops in 3 days!

What's your longest function? 💬 Drop the line count in the comments - let's see who has the biggest monster to refactor! 😄

Sharing is caring! 📤 Share this with your dev team - everyone benefits from cleaner functions.


This is Part 2 of the 7-part "JavaScript Clean Code Mastery" series.

← Part 1: Naming & Variables | Part 3: Modern JavaScript Features →

Tags: #JavaScript #CleanCode #Functions #ArrowFunctions #WebDevelopment #Programming #ES6 #SoftwareEngineering #Tutorial

Top comments (0)