Mastering Clean Code: Essential Practices for Developers
Tags: webdev, programming, cleancode, learning
Published: 2024-06-15 16:56:04

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

- Descriptive Variable Names:
```
// 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.

- Consistent Formatting:

```
// 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.

- Avoiding Magic Numbers:

```
// 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.

- Single Responsibility Principle:

```
// 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.

- Error Handling:

```
// 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.

- Comments and Documentation:

```
// 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.

- Proper Modularity:

```
// 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.

- DRY Principle (Don't Repeat Yourself):

```
// 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.

- Meaningful Function Names:

```
// 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.

- Testability:

```
// 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.

- Proper Use of Data Structures:

```
// 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.

- Handling Asynchronous Operations:

```
// 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.

- Dependency Management:

```
// 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.

- Performance Optimization:

```
// 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.

- Proper Error Handling in Node.js APIs:

```
// 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.

- Efficient File System Operations:

```
// 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.

- Efficient Memory Management:

```
// 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.

- Proper Module Exporting and Importing:

```
// 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.

- Asynchronous Control Flow:

```
// 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.