Clean Code Guidelines
- Guard Clauses / Fail-Fast: Handle invalid or special cases early and return immediately.
- Use switch, pattern matching, strategy, or a lookup table for branching on one discriminator.
- Command–Query Separation (CQS): A function either returns data or causes effects, never both.
Guard Clauses / Fail-Fast
Rule: Guard Clauses / Fail-Fast: Handle invalid or special cases early and return immediately.
❌ Bad Example: Deeply nested conditions
function calculateDiscount(price, customerType, coupon) {
if (price > 0) {
if (customerType === 'premium') {
if (coupon === 'SAVE20') {
return price * 0.8; // 20% discount
} else {
return price * 0.9; // 10% discount
}
} else {
return price; // no discount
}
} else {
throw new Error('Invalid price');
}
}
Problems:
- Hard to follow the main flow
- Error handling is buried at the bottom
- More cognitive load to understand
✅ Good Example: Guard clauses with early returns
function calculateDiscountWithGuards(price, customerType, coupon) {
if (price <= 0) throw new Error('Invalid price');
if (customerType !== 'premium') return price;
return coupon === 'SAVE20' ? price * 0.8 : price * 0.9;
}
Benefits:
- Invalid inputs handled immediately
- Main logic is clear and at the top level
- Reduced nesting and cognitive complexity
Branching on One Discriminator
Rule: Use switch, pattern matching, strategy, or a lookup table for branching on one discriminator.
❌ Bad Example: Multiple if-else statements
function getShippingCost(shippingMethod) {
if (shippingMethod === 'standard') {
return 5.99;
} else if (shippingMethod === 'express') {
return 12.99;
} else if (shippingMethod === 'overnight') {
return 24.99;
} else if (shippingMethod === 'pickup') {
return 0;
} else {
throw new Error('Invalid shipping method');
}
}
Problems:
- Repetitive if-else chain structure
- Hard to maintain when adding new methods
- Not easily extensible
✅ Good Example: Lookup table approach
function getShippingCost(shippingMethod) {
const shippingCosts = {
standard: 5.99,
express: 12.99,
overnight: 24.99,
pickup: 0
};
if (!(shippingMethod in shippingCosts)) {
throw new Error('Invalid shipping method');
}
return shippingCosts[shippingMethod];
}
✅ Alternative: Switch statement
function getShippingCost(shippingMethod) {
switch (shippingMethod) {
case 'standard':
return 5.99;
case 'express':
return 12.99;
case 'overnight':
return 24.99;
case 'pickup':
return 0;
default:
throw new Error('Invalid shipping method');
}
}
Benefits:
- Cleaner, more readable code
- Clear mapping between discriminator and action
- Better performance with lookup tables
Command–Query Separation (CQS)
Rule: Command–Query Separation (CQS): A function either returns data or causes effects, never both.
❌ Bad Example: Function that both queries and commands
function removeAndCountItems(array, item) {
let count = 0;
for (let i = array.length - 1; i >= 0; i--) {
if (array[i] === item) {
array.splice(i, 1); // Mutates array (command)
count++;
}
}
return count; // Returns data (query)
}
Problems:
- Hard to predict if function will change state
- Difficult to test and reason about
- Unclear intent from function name
✅ Good Example: Separated commands and queries
// Query: Returns data without mutation
function countItems(array, item) {
return array.filter(x => x === item).length;
}
// Command: Only mutates, returns void
function removeAllItems(array, item) {
for (let i = array.length - 1; i >= 0; i--) {
if (array[i] === item) {
array.splice(i, 1);
}
}
}
Benefits:
- Clear separation of concerns
- Predictable behavior - queries are safe, commands have effects
- Easier to test and debug
- Functions have single, clear purpose