Mastering the Craft: 10 Key Highlights from the “Clean Code” Bible for Every Developer
Learn the Art of Clean Coding with Javascript
Every programmer, regardless of their level of experience, can benefit from the wisdom in Robert C. Martin’s book, “Clean Code.” It has been curated as one of the must-read book for every software engineer. As a software developer, crafting clean, efficient, and maintainable code should be your top priority. In this article, we’ll explore the key highlights and principles outlined in “Clean Code,” helping you become a better developer and transforming your programming journey for the better.

There are 10 key areas in total that I think everyone should beware of to elevate the coding skills to the next level. They are Meaningful Names, Functions, Function Arguments, Comments, Formatting, Objects and Data Structures, Error Handling, Unit Tests, Classes and Code Smells.
1. Meaningful Names
TL;DR Choose clear, expressive names for variables, functions, and classes to improve readability and maintainability.
Using meaningful names for variables, functions, and classes in your code is crucial. This makes the code more readable, maintainable, and understandable for both the original developer and others who may work with the code in the future.
Here’s an example to illustrate the difference between poor and meaningful naming in JavaScript:
Poor Naming:
let d; // time in days
const r = 5; // interest rate
const p = 1000; // principal amount
function i(p, r, d) {
return (p * r * d) / 36500;
}
const interest = i(p, r, d);
Meaningful Naming:
const principalAmount = 1000;
const annualInterestRate = 5;
const investmentPeriodInDays = 30;
function calculateInterest(principal, rate, days) {
return (principal * rate * days) / 36500;
}
const interest = calculateInterest(principalAmount, annualInterestRate, investmentPeriodInDays);
In the meaningful naming example, we can see:
- Variables have descriptive names, such as
principalAmount
,annualInterestRate
, andinvestmentPeriodInDays
. These names make it clear what values they represent. - The function has a meaningful name,
calculateInterest
, which clearly indicates what it does. - The arguments for the function also have clear names, like
principal
,rate
, anddays
, making it easy to understand what inputs the function requires.
Using meaningful names greatly improves the readability of the code, making it easier for others (and yourself) to understand and maintain it.
2. Functions
TL;DR Functions should be small, focused, and have a single responsibility. They should do one thing and do it well.
Having small, focused functions that adhere to the Single Responsibility Principle (SRP) is another highlight of the book. A function should do one thing and do it well, which makes the code more readable, maintainable, and testable.
Here’s an example to illustrate the difference between a function that doesn’t follow SRP and one that does in JavaScript:
Without Single Responsibility Principle:
function processEmployeeData(employeeData) {
const fullName = `${employeeData.firstName} ${employeeData.lastName}`;
// Calculate age based on birthdate
const today = new Date();
const birthDate = new Date(employeeData.birthDate);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDifference = today.getMonth() - birthDate.getMonth();
if (monthDifference < 0 || (monthDifference === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
console.log(`Full name: ${fullName}`);
console.log(`Age: ${age}`);
}
const employeeData = {
firstName: 'John',
lastName: 'Doe',
birthDate: '1990-06-15',
};
processEmployeeData(employeeData);
With Single Responsibility Principle:
function getFullName(employeeData) {
return `${employeeData.firstName} ${employeeData.lastName}`;
}
function calculateAge(birthDate) {
const today = new Date();
const parsedBirthDate = new Date(birthDate);
let age = today.getFullYear() - parsedBirthDate.getFullYear();
const monthDifference = today.getMonth() - parsedBirthDate.getMonth();
if (monthDifference < 0 || (monthDifference === 0 && today.getDate() < parsedBirthDate.getDate())) {
age--;
}
return age;
}
function displayEmployeeInformation(employeeData) {
const fullName = getFullName(employeeData);
const age = calculateAge(employeeData.birthDate);
console.log(`Full name: ${fullName}`);
console.log(`Age: ${age}`);
}
const employeeData = {
firstName: 'John',
lastName: 'Doe',
birthDate: '1990-06-15',
};
displayEmployeeInformation(employeeData);
In the Single Responsibility Principle example, we can see that:
- The original
processEmployeeData
function is split into three smaller, focused functions:getFullName
,calculateAge
, anddisplayEmployeeInformation
. - Each function performs a single task:
getFullName
returns the full name,calculateAge
returns the age, anddisplayEmployeeInformation
displays the information to the console. - The functions are more readable, maintainable, and testable since they are small and focused.
By adhering to the Single Responsibility Principle, we make the code easier to understand, maintain, and test, which leads to higher quality software.
3. Function Arguments
TL;DR Keep the number of arguments to a minimum and avoid output arguments when possible. This enhances the readability and understandability of the code.
Minimising the number of function arguments and avoiding output arguments are important. They help improving code readability and understandability.
Having fewer arguments makes it easier to understand the purpose of a function and how it should be used. Additionally, output arguments, which modify the input variables passed by reference, can lead to confusion and unintended side effects.
Multiple Arguments:
function calculateTotal(price, taxRate, discountRate) {
const taxAmount = price * taxRate;
const discountAmount = price * discountRate;
return price + taxAmount - discountAmount;
}
const price = 100;
const taxRate = 0.1; // 10%
const discountRate = 0.05; // 5%
const total = calculateTotal(price, taxRate, discountRate);
Fewer Arguments (Using Object):
function calculateTotal({ price, taxRate, discountRate }) {
const taxAmount = price * taxRate;
const discountAmount = price * discountRate;
return price + taxAmount - discountAmount;
}
const product = {
price: 100,
taxRate: 0.1, // 10%
discountRate: 0.05, // 5%
};
const total = calculateTotal(product);
In the Fewer Arguments example, we can see that:
- The function takes a single argument, an object containing the relevant properties (
price
,taxRate
, anddiscountRate
), instead of three separate arguments. - By passing an object, we make the function more flexible, allowing for additional properties to be added in the future without changing the function signature.
- The code becomes more readable as the properties are grouped together logically within the object.
Next, let’s consider an example demonstrating the avoidance of output arguments:
With Output Argument:
function calculateAreaAndPerimeter(width, height, result) {
result.area = width * height;
result.perimeter = 2 * (width + height);
}
const dimensions = { width: 10, height: 5 };
const result = {};
calculateAreaAndPerimeter(dimensions.width, dimensions.height, result);
Without Output Argument:
function calculateAreaAndPerimeter(width, height) {
const area = width * height;
const perimeter = 2 * (width + height);
return { area, perimeter };
}
const dimensions = { width: 10, height: 5 };
const result = calculateAreaAndPerimeter(dimensions.width, dimensions.height);
In the second example, we can see that:
- The function returns an object containing the
area
andperimeter
properties, instead of modifying an input argument. - The function is easier to understand and use because it avoids side effects and follows a more common pattern of returning a result.
By minimising the number of function arguments and avoiding output arguments, we make our code more readable, understandable, and maintainable.
4. Comments
TL;DR Comments should be concise and meaningful. They should be used to explain the purpose of code and to clarify complex logic, but should not be used as a substitute for writing clear code.
Comments should be used appropriately in your code. Comments should be concise, meaningful, and used to explain the purpose or intent of the code, or to clarify complex logic. However, comments should not be used as a substitute for writing clear and readable code.
Poor Comments:
// Calculate total
function calcTot(a, b, c) {
// Add a and b
const t1 = a + b;
// Subtract c
const t2 = t1 - c;
// Return result
return t2;
}
In this example, comments are used excessively, and the code has poor naming conventions. The comments describe what the code does rather than why it does it. Now let’s refactor the code with better naming and more effective comments:
Effective Comments:
function calculateTotal(price, tax, discount) {
const totalPriceBeforeDiscount = price + tax;
// Apply the discount to the total price before discount
const finalPrice = totalPriceBeforeDiscount - discount;
return finalPrice;
}
In the improved example, we can see that:
- Variables and the function have descriptive names, which makes the code self-explanatory, reducing the need for comments.
- Comments are used sparingly, only to clarify the purpose of applying the discount.
- The code is easier to read and understand because the meaningful names and effective comments provide context.
Keep in mind that comments can become outdated or incorrect as code evolves, so it’s essential to keep them updated and relevant. When writing comments, focus on explaining the “why” behind the code or providing context for complex logic that might not be apparent from the code itself.
5. Formatting
TL;DR Consistent formatting, including indentation and spacing, improves code readability and makes it easier for others to understand and maintain.
Proper formatting improves code readability and makes it easier for others (and yourself) to understand and maintain the code.
Poorly Formatted Code:
function calculateTotal(price,tax,discount){const totalPriceBeforeDiscount=price+tax;const finalPrice=totalPriceBeforeDiscount-discount;return finalPrice;}
const productPrice=100;const taxAmount=15;const discountAmount=10;const total=calculateTotal(productPrice,taxAmount,discountAmount);console.log("Total:",total);
Well-Formatted Code:
function calculateTotal(price, tax, discount) {
const totalPriceBeforeDiscount = price + tax;
const finalPrice = totalPriceBeforeDiscount - discount;
return finalPrice;
}
const productPrice = 100;
const taxAmount = 15;
const discountAmount = 10;
const total = calculateTotal(productPrice, taxAmount, discountAmount);
console.log("Total:", total);
In the well-formatted example, we can see:
- Consistent indentation: Each level of code is indented with an appropriate number of spaces or tabs, making it easier to see the structure of the code.
- Spacing around operators: There is space around operators such as
=
,+
,-
, making the code easier to read. - Line breaks: Line breaks are used to separate variable declarations, function calls, and other statements, which improves readability.
- Consistent use of quotes: The same type of quotes (double or single) should be used consistently throughout the code.
- Clear separation between code blocks: Blank lines are used to separate function declarations, variable declarations, and other logical sections of the code.
To maintain consistent formatting throughout your codebase, consider using tools like ESLint and Prettier to automatically format your code and enforce a consistent style.
6. Objects and Data Structures
TL;DR Encapsulation should be employed to hide implementation details and expose only necessary information, making it easier to reason about and change the code.
Encapsulation makes it easier to reason about and change the code since it isolates the impact of changes to a specific component or module.
Without Encapsulation:
const account = {
balance: 1000,
deposit: function (amount) {
this.balance += amount;
},
withdraw: function (amount) {
if (amount <= this.balance) {
this.balance -= amount;
} else {
console.log('Insufficient balance');
}
},
};
account.balance += 500; // Directly modifying the balance
account.withdraw(200);
With Encapsulation:
const createBankAccount = (initialBalance) => {
let balance = initialBalance;
const deposit = (amount) => {
balance += amount;
};
const withdraw = (amount) => {
if (amount <= balance) {
balance -= amount;
} else {
console.log('Insufficient balance');
}
};
const getBalance = () => balance;
return { deposit, withdraw, getBalance };
};
const account = createBankAccount(1000);
account.deposit(500); // Using provided methods to interact with the balance
account.withdraw(200);
const currentBalance = account.getBalance();
In the encapsulated example, we can see that:
- The
balance
variable is not directly exposed; it is hidden within the closure created by thecreateBankAccount
function. - Public methods are provided to interact with the account (
deposit
,withdraw
,getBalance
), which control access and manipulation of thebalance
. - Users of the
account
object are limited to the provided methods and cannot directly modify thebalance
, ensuring the integrity of the object's state.
By utilizing encapsulation, we create a clear separation between an object’s internal state and the external interface used to interact with that object. This makes the code easier to understand, maintain, and modify, as changes to the internal implementation will have a limited impact on the code that uses the object.
7. Error Handling
TL;DR Error handling should be treated as a separate concern and should not be mixed with the main logic of the code. Using exceptions instead of error codes is recommended.
Proper error handling involves anticipating potential issues and handling them gracefully, making it easier to understand, diagnose, and fix problems when they occur. Using exceptions and appropriate error messages can help with this process.
Without Error Handling:
function divide(a, b) {
return a / b;
}
const result = divide(10, 0);
console.log('Result:', result); // Output: 'Result: Infinity'
In the above example, there is no error handling for division by zero, leading to an unexpected output of “Infinity.” Now let’s add error handling to handle this case:
With Error Handling:
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed.');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log('Result:', result);
} catch (error) {
console.error('An error occurred:', error.message);
// Output: 'An error occurred: Division by zero is not allowed.'
}
In the improved example, we can see that:
- The
divide()
function checks for division by zero and throws anError
with a meaningful message if it occurs. - The calling code uses a
try
/catch
block to handle the exception, which allows for graceful handling of the error and prevents the application from crashing. - The error message is helpful and indicates the specific issue that occurred, making it easier to diagnose and fix.
By implementing proper error handling in your code, it’s easier to understand, diagnose, and fix problems when they occur, and helps to ensure a better user experience when issues arise.
8. Unit Tests
TL;DR Writing and maintaining a comprehensive suite of unit tests is essential for ensuring code quality and reducing the risk of bugs being introduced.
Three Laws of TTD
- You may not write production code until you have written a failing unit test.
- You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
- You may not write more production code than is sufficient to pass the currently failing test.
Let’s write tests for a simple Stack
class:
const Stack = require('./stack');
test('push() should add an item to the stack', () => {
const stack = new Stack();
stack.push(1);
expect(stack.peek()).toBe(1);
});
test('pop() should remove the top item from the stack', () => {
const stack = new Stack();
stack.push(1);
stack.push(2);
expect(stack.pop()).toBe(2);
});
test('peek() should return the top item without removing it', () => {
const stack = new Stack();
stack.push(1);
stack.push(2);
expect(stack.peek()).toBe(2);
expect(stack.peek()).toBe(2); // Repeating the call to ensure the item was not removed.
});
test('isEmpty() should return true if the stack is empty', () => {
const stack = new Stack();
expect(stack.isEmpty()).toBe(true);
stack.push(1);
expect(stack.isEmpty()).toBe(false);
});
// Law 1: Write a failing test before writing production code.
// Law 2: Write just enough of a test to fail.
test('pop() should throw an error if the stack is empty', () => {
const stack = new Stack();
expect(() => stack.pop()).toThrow(Error);
});
test('peek() should throw an error if the stack is empty', () => {
const stack = new Stack();
expect(() => stack.peek()).toThrow(Error);
});
// Law 3: Write just enough production code to pass the test.
By adhering to the Three Laws of TDD, we incrementally build the functionality of the Stack
class, making sure that each step is guided by a failing test before implementing the corresponding production code. This process ensures that our code has good test coverage and is designed with testability in mind.
Keeping tests clean:
- Make tests readable by using clear and descriptive test names, as shown in the examples above.
- Keep tests focused on a single aspect of the code being tested.
- Ensure tests are independent and can be run in any order. In the examples above, each test creates its instance of
Stack
and does not rely on the outcome of other tests.
Clean tests provide several benefits:
- They serve as living documentation for your code, helping developers understand how the code is supposed to behave.
- They help you catch regressions and bugs early in the development process, saving time and effort.
- They make it easier to refactor and improve your code with confidence since the tests ensure that existing functionality still works as expected.
By treating your tests as an essential part of your codebase, you’ll be able to build higher-quality software with fewer bugs and a more stable foundation for future development.
Classes
TL;DR Classes should be small and focused, adhering to the Single Responsibility Principle. This makes them easier to understand, maintain, and extend.
Clean Code principles mention encapsulation, small classes, and the Single Responsibility Principle (SRP) when working with classes.
Let’s say we have an e-commerce application where users can place orders. We’ll create a simple Product
class and an Order
class that follows the mentioned principles.
product.js
:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
getName() {
return this.name;
}
getPrice() {
return this.price;
}
}
module.exports = Product;
In the Product
class, we follow the SRP and Encapsulation principles by providing getter methods for the name
and price
properties. The class is small, focused, and has a single responsibility: to represent a product.
order.js
:
class Order {
constructor() {
this.items = [];
}
addItem(product, quantity) {
this.items.push({ product, quantity });
}
getTotal() {
return this.items.reduce(
(total, item) => total + item.product.getPrice() * item.quantity,
0
);
}
}
module.exports = Order;
In the Order
class, we also adhere to the SRP and Encapsulation principles. The class is responsible for managing the order items and calculating the total price. We avoid exposing the internal structure of the items
array by providing specific methods for adding items and getting the total.
Now let’s use the Product
and Order
classes in another JavaScript file:
index.js
:
const Product = require('./product');
const Order = require('./order');
const product1 = new Product('Laptop', 1000);
const product2 = new Product('Mouse', 50);
const order = new Order();
order.addItem(product1, 1);
order.addItem(product2, 2);
console.log('Order Total:', order.getTotal()); // Output: 'Order Total: 1100'
In this example, we can see that:
- The
Product
andOrder
classes follow the SRP, each having a specific responsibility. - Both classes are small, with a limited number of methods and properties, which makes them easier to understand and maintain.
- Encapsulation is applied, hiding the implementation details and exposing only the necessary information through public methods.
By adhering to the Clean Code principles when working with classes, we create code that is more maintainable, easier to understand, and promotes better code organisation.
10. Code Smells
TL;DR Be aware of and address common “code smells” — indicators that the code may have design or structural issues that need to be addressed.
Code smells are patterns or symptoms in the code that indicate there may be design or structural issues. They don’t always directly cause bugs but can lead to a codebase that’s hard to maintain, extend, and understand. Here are some common code smells and their examples in JavaScript:
1. Long Functions
Functions that are too long and try to do too much can be hard to understand and maintain.
function processData(data) {
// Step 1: validation
if (data === null || typeof data !== 'object') {
return null;
}
// Step 2: normalization
data = data.trim().toLowerCase();
// Step 3: processing
const words = data.split(' ');
const wordCounts = {};
for (const word of words) {
if (wordCounts[word]) {
wordCounts[word]++;
} else {
wordCounts[word] = 1;
}
}
// Step 4: output
console.log('Word counts:', wordCounts);
}
A better approach would be to split this function into smaller, focused functions:
function validateData(data) {
if (data === null || typeof data !== 'object') {
return null;
}
return data;
}
function normalizeData(data) {
return data.trim().toLowerCase();
}
function countWords(data) {
const words = data.split(' ');
const wordCounts = {};
for (const word of words) {
if (wordCounts[word]) {
wordCounts[word]++;
} else {
wordCounts[word] = 1;
}
}
return wordCounts;
}
function outputResult(wordCounts) {
console.log('Word counts:', wordCounts);
}
function processData(data) {
data = validateData(data);
if (!data) return;
data = normalizeData(data);
const wordCounts = countWords(data);
outputResult(wordCounts);
}
2. Large Classes
Classes that are too large and have too many responsibilities can be hard to understand and maintain.
class UserManager {
constructor(users) {
this.users = users;
}
findUser(id) {
return this.users.find(user => user.id === id);
}
getUserEmail(id) {
const user = this.findUser(id);
return user ? user.email : null;
}
// ... many other user-related methods
log(message) {
console.log(message);
}
// ... many other logging methods
}
In this case, we should split the UserManager
class into two separate classes, each with a single responsibility:
class UserManager {
constructor(users) {
this.users = users;
}
findUser(id) {
return this.users.find(user => user.id === id);
}
getUserEmail(id) {
const user = this.findUser(id);
return user ? user.email : null;
}
// ... other user-related methods
}
class Logger {
log(message) {
console.log(message);
}
// ... other logging methods
}
3. Duplicate Code
Duplicate code makes it hard to maintain and introduce changes since the same logic may need to be updated in multiple places.
function processUser(user) {
// ... some processing
console.log(`Processed user: ${user.name}`);
}
function processProduct(product) {
// ... some processing
console.log(`Processed product: ${product.name}`);
}
In this case, we should extract the common code into a separate function:
function logProcessingResult(type, name) {
console.log(`Processed ${type}: ${name}`);
}
function processUser(user) {
// ... some processing
logProcessingResult('user', user.name);
}
function processProduct(product) {
// ... some processing
logProcessingResult('product', product.name);
}
4. Magic Numbers or Strings
Using unexplained numbers or strings in the code can make it hard to understand the purpose or meaning of those values.
function calculateTotal(price, quantity) {
return price * quantity * 1.07; // What does 1.07 mean?
}
A better approach would be to use a constant to represent the value with a meaningful name:
const TAX_RATE = 1.07;
function calculateTotal(price, quantity) {
return price * quantity * TAX_RATE;
}
5. Deep Nesting
Deeply nested code blocks can be hard to understand and follow.
function processOrder(order) {
if (order) {
if (order.items) {
if (order.items.length > 0) {
// ... process the order
} else {
console.log('No items in the order.');
}
} else {
console.log('Order is missing items.');
}
} else {
console.log('No order to process.');
}
}
We can refactor the code to reduce nesting by using early returns:
function processOrder(order) {
if (!order) {
console.log('No order to process.');
return;
}
if (!order.items) {
console.log('Order is missing items.');
return;
}
if (order.items.length === 0) {
console.log('No items in the order.');
return;
}
// ... process the order
}
By identifying and addressing code smells, you can improve the overall quality of your codebase, making it easier to understand, maintain, and extend.
To know more about code refactoring, check out this refactoring guide:
By following the key highlights and principles from “Clean Code” by Robert C. Martin, you’ll be well on your way to crafting elegant, efficient, and maintainable code. Remember that clean code is an ongoing journey, and it takes consistent effort to master the craft. Embrace these practices and watch as your code becomes clearer, more manageable, and more enjoyable to work with.
To know more about my backend learning path, check out my journey here:
Level Up Coding
Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the Level Up Coding publication
- 💰 Free coding interview course ⇒ View Course
- 🔔 Follow us: Twitter | LinkedIn | Newsletter
🚀👉 Join the Level Up talent collective and find an amazing job