Mastering Clean Code: Essential Practices for Developers
Mahabubur Rahman
Software Engineer | Full Stack Developer | Competitive Programmer | Data Science Practitioner
Clean code is the cornerstone of every successful software project. As a developer, your ability to write clean, maintainable code is crucial for the efficiency and longevity of your applications. In this article, we'll delve into ten examples of good and bad coding practices in JavaScript, highlighting the importance of writing clean code and providing actionable insights to help you level up your development skills.
Examples
// Good:
const totalPrice = calculateTotalPrice(quantity, unitPrice);
// Bad:
const t = calcPrice(q, uP);
In a good example, variable names are descriptive and convey their purpose clearly, enhancing code readability. Conversely, the bad example uses cryptic abbreviations, making it difficult for others to understand the code's intent.
// Good:
function greet(name) {
return `Hello, ${name}!`;
}
// Bad:
function greet(name){
return `Hello, ${name}!`
}
Consistent formatting improves code readability and maintainability. In a good example, proper indentation and spacing are employed, enhancing code structure. Conversely, the bad example lacks consistency, making the code harder to follow.
// Good:
const TAX_RATE = 0.1;
const totalPrice = subtotal + (subtotal * TAX_RATE);
// Bad:
const totalPrice = subtotal + (subtotal * 0.1);
Magic numbers obscure the meaning of values and make code harder to maintain. In the good example, constants are used to represent magic numbers, improving code clarity and maintainability.
// Good:
function calculateTotalPrice(quantity, unitPrice) {
return quantity * unitPrice;
}
function formatPrice(price) {
return `$${price.toFixed(2)}`;
}
// Bad:
function calculateAndFormatTotalPrice(quantity, unitPrice) {
const totalPrice = quantity * unitPrice;
return `$${totalPrice.toFixed(2)}`;
}
Functions should have a single responsibility to promote code reusability and maintainability. In the good example, each function performs a specific task, adhering to the Single Responsibility Principle. Conversely, the bad example violates this principle by combining multiple responsibilities into one function.
// Good:
function fetchData(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
console.error('Error fetching data:', error);
throw error;
});
}
// Bad:
function fetchData(url) {
return fetch(url)
.then(response => response.json())
.catch(error => console.error(error));
}
Proper error handling improves code robustness and helps identify and resolve issues more effectively. In the good example, errors are handled gracefully, providing meaningful feedback to developers. Conversely, the bad example lacks comprehensive error handling, potentially leading to silent failures.
// Good:
// Calculate the total price based on quantity and unit price
function calculateTotalPrice(quantity, unitPrice) {
return quantity * unitPrice;
}
// Bad:
function calculateTotalPrice(quantity, unitPrice) {
// calculate total price
return quantity * unitPrice;
}
Comments and documentation enhance code understandability and facilitate collaboration among developers. In a good example, clear comments describe the purpose of the function, aiding in code comprehension. Conversely, the bad example provides vague comments that add little value.
// Good:
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// Bad:
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
Modular code promotes reusability and maintainability by organizing functionality into cohesive units. In the good example, functions are properly encapsulated and exported, facilitating code reuse. Conversely, the bad example lacks modularization, making it harder to manage and scale.
// Good:
const greeting = 'Hello';
function greet(name) {
return `${greeting}, ${name}!`;
}
// Bad:
function greet(name) {
const greeting = 'Hello';
return `${greeting}, ${name}!`;
}
Repetitive code increases the risk of errors and makes maintenance challenging. In a good example, repetitive strings are extracted into a constant, adhering to the DRY principle and improving code maintainability. Conversely, the bad example redundantly defines the greeting within the function.
// Good:
function calculateArea(radius) {
return Math.PI * radius ** 2;
}
// Bad:
function calc(r) {
return Math.PI * r ** 2;
}
Function names should accurately reflect their purpose to enhance code readability. In the good example, the function name "calculateArea" clearly indicates its functionality. Conversely, the bad example uses a cryptic abbreviation ("calc"), making it unclear what the function does.
领英推荐
// Good:
function sum(a, b) {
return a + b;
}
module.exports = sum;
// Bad:
function sum(a, b) {
console.log(a + b);
}
Writing testable code facilitates automated testing, ensuring code reliability and stability. In the good example, the function is exported for testing purposes, enabling easy test setup and execution. Conversely, the bad example contains side effects (console.log), making it challenging to test the function's behavior.
// Good:
const studentGrades = [90, 85, 95, 88];
const averageGrade = studentGrades.reduce((total, grade) => total + grade, 0) / studentGrades.length;
// Bad:
const grade1 = 90;
const grade2 = 85;
const grade3 = 95;
const grade4 = 88;
const averageGrade = (grade1 + grade2 + grade3 + grade4) / 4;
Utilizing appropriate data structures enhances code readability and maintainability. In the good example, an array is used to store student grades, allowing for easy manipulation and calculation. Conversely, the bad example relies on individual variables, leading to repetitive and error-prone code.
// Good:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
// Bad:
function fetchData(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
console.error('Error fetching data:', error);
throw error;
});
}
Proper handling of asynchronous operations ensures code reliability and robustness. In a good example, async/await syntax is used to simplify asynchronous code and handle errors gracefully. Conversely, the bad example uses nested promises, leading to callback hell and decreased code readability.
// Good:
import { format } from 'date-fns';
// Bad:
const dateFns = require('date-fns');
Effective dependency management promotes code modularity and scalability. In a good example, ES6 import syntax is used to import only the required functionality from the 'date-fns' library, reducing unnecessary imports and improving performance. Conversely, the bad example uses CommonJS require syntax, which imports the entire 'date-fns' module, potentially bloating the application bundle.
// Good:
const sortedNumbers = [5, 2, 8, 1, 9];
sortedNumbers.sort((a, b) => a - b);
// Bad:
const unsortedNumbers = [5, 2, 8, 1, 9];
const sortedNumbers = unsortedNumbers.sort();
Optimizing code for performance ensures efficient execution and enhances user experience. In a good example, the sort() method is called with a custom comparison function to sort numbers in ascending order, resulting in better performance compared to the default sorting algorithm. Conversely, the bad example relies on the default sorting algorithm, which may not be the most efficient for numerical arrays.
// Good:
app.get('/user/:id', async (req, res) => {
try {
const user = await getUserById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Bad:
app.get('/user/:id', async (req, res) => {
const user = await getUserById(req.params.id);
if (!user) {
res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
Proper error handling is crucial in Node.js APIs to ensure robustness and reliability. In the good example, errors are caught and logged, and appropriate HTTP status codes are returned to the client. Conversely, the bad example fails to handle errors, potentially resulting in unhandled promise rejections and inconsistent error responses.
// Good:
const fs = require('fs').promises;
async function readFile(filePath) {
try {
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
} catch (error) {
console.error('Error reading file:', error);
}
}
// Bad:
const fs = require('fs');
function readFile(filePath) {
fs.readFile(filePath, 'utf-8', (error, data) => {
if (error) {
console.error('Error reading file:', error);
return;
}
console.log(data);
});
}
Using promises in file system operations enhances code readability and simplifies error handling. In a good example, fs.promises.readFile() is used to read a file asynchronously, and errors are handled using try-catch. Conversely, the bad example uses the callback-based approach, which can lead to callback hell and less readable code.
// Good:
const stream = fs.createReadStream('bigfile.txt');
stream.pipe(response);
// Bad:
fs.readFile('bigfile.txt', (error, data) => {
if (error) {
console.error('Error reading file:', error);
return;
}
response.write(data);
});
Using streams for large file processing in Node.js conserves memory and improves performance. In a good example, fs.createReadStream() and stream.pipe() are used to efficiently stream data from a file to an HTTP response. Conversely, the bad example reads the entire file into memory before writing it to the response, which can lead to memory issues for large files.
// Good:
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
// Bad:
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
Consistent module exporting and importing practices improve code readability and maintainability. In the good example, module.exports is used to export an object containing functions, while in the bad example, exports are used directly. Although both methods work, sticking to one convention enhances code consistency.
// Good:
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
}
// Bad:
function processItems(items) {
items.forEach(item => {
processItem(item);
});
}
Proper asynchronous control flow ensures that operations are executed sequentially or concurrently as needed. In a good example, an async function is used with a for...of the loop to process items sequentially, awaiting each operation. Conversely, the bad example uses forEach, which does not handle asynchronous operations well and may lead to unexpected behavior.