DEV Community

Cover image for Mastering Clean Code: Essential Practices for Developers
Mahabubur Rahman
Mahabubur Rahman

Posted on

Mastering Clean Code: Essential Practices for Developers

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);
Enter fullscreen mode Exit fullscreen mode
// Bad:

const t = calcPrice(q, uP);
Enter fullscreen mode Exit fullscreen mode

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}!`;
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function greet(name){
return `Hello, ${name}!`
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// Bad:

const totalPrice = subtotal + (subtotal * 0.1);

Enter fullscreen mode Exit fullscreen mode

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)}`;
}

Enter fullscreen mode Exit fullscreen mode
// Bad:

function calculateAndFormatTotalPrice(quantity, unitPrice) {
    const totalPrice = quantity * unitPrice;
    return `$${totalPrice.toFixed(2)}`;
}
Enter fullscreen mode Exit fullscreen mode

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;
        });
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function fetchData(url) {
    return fetch(url)
        .then(response => response.json())
        .catch(error => console.error(error));
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function calculateTotalPrice(quantity, unitPrice) {
    // calculate total price
    return quantity * unitPrice;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}
Enter fullscreen mode Exit fullscreen mode

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}!`;
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function greet(name) {
    const greeting = 'Hello';
    return `${greeting}, ${name}!`;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function calc(r) {
    return Math.PI * r ** 2;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// Bad:

function sum(a, b) {
    console.log(a + b);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// Bad:

const grade1 = 90;
const grade2 = 85;
const grade3 = 95;
const grade4 = 88;
const averageGrade = (grade1 + grade2 + grade3 + grade4) / 4;
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode
// 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;
        });
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode
// Bad:

const dateFns = require('date-fns');
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// Bad:

const unsortedNumbers = [5, 2, 8, 1, 9];
const sortedNumbers = unsortedNumbers.sort();
Enter fullscreen mode Exit fullscreen mode

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' });
    }
});
Enter fullscreen mode Exit fullscreen mode
// 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);
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode
// 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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

// Bad:

fs.readFile('bigfile.txt', (error, data) => {
    if (error) {
        console.error('Error reading file:', error);
        return;
    }
    response.write(data);
});
Enter fullscreen mode Exit fullscreen mode

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
};

Enter fullscreen mode Exit fullscreen mode
// Bad:

exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode
// Bad:

function processItems(items) {
    items.forEach(item => {
        processItem(item);
    });
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (13)

Collapse
 
syeo66 profile image
Red Ochsenbein (he/him)

I have some issues with your examples:

First, in your comments example "Calculate the total price based on quantity and unit price" just simply restates what the function already defines anyways. I'd consider this to be a bad comment. A good comment should state something about the context, about the 'why' this piece of code exists. What it does should be as clear as possible by it's definition.

Also your "Asynchronous Control Flow" example is basically just making the async calls synchronous without good reason. According to the function call whatever the asynchronous code does seems not to be dependent on a previous function call. (If it does, you'd probably have to rethink what you do, because it should probably be batched together into something like a transaction). So, based on that why limit the async code to only one execution at once?

async function processItems(items) {
    const promises = [];
    for (const item of items) {
        promises.push(processItem(item));
    }
    await Promise.all(promises);
}
Enter fullscreen mode Exit fullscreen mode

Is probably closer to what you want. Also - as @thenickest mentioned - it's missing error handling since every async execution could get rejected.

Collapse
 
willian_eckstein profile image
Willian Eckstein

Good I was about to comment about the comments section, but you was faster 😃. A redundant comment could became a problem because with time people tend to update the code and forget to update the comments, so I would avoid comments on code and try to make the declarations more comprehensible as possible.

But @mahabubr, very good article anyway, congrats.

Collapse
 
mahabubr profile image
Mahabubur Rahman

thank you for share your prospective

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

Your 'Performance Optimization' example is wrong / nonsensical....

  • The 'Good' example runs slower than the bad example - therefore has worse performance
  • The 'Bad' example consistently runs faster (see benchmark here)
  • The two examples do different things (the good example does a numerical sort, whilst the bad example does a string sort) - rendering any performance comparison meaningless.
Collapse
 
nippysaurus profile image
Michael Dawson

Great post! In my experience we need more people taking clean code seriously and helping raise the tide 😁

Often when I talk to people about these type of things I get responses like “I don’t care about writing beautiful code, we have real work to do”, so I feel like it’s always important to focus on why these principles are valuable. They help us build systems which can adapt to change more easily. I find that most of the value comes from just putting in a little effort with SRP and variable naming ✌️

Collapse
 
cbus818 profile image
Anthony

I don't agree with the magic numbers example. I don't understand how using a constant violates clean code principles. In your bad example, you use TAX_RATE. If the tax rate changed, you would have to go through your function and update it in multiple spots. In a large code base, there is the risk of missing a value.

I agree with the other devs regarding your comment examples. I personally only add comments to add clarity for a decision made that isn't easy to articulate in the code itself. The code itself would be written in a way that explains its function.

Collapse
 
diu_gach profile image
Diu Gach

Since taxrate is stored in a constant variable, that will only take a single change, change the taxRate variable to another value and that's all, my opinion though.

Collapse
 
paul_danger_kile profile image
Paul Danger Kile • Edited

Please change the name of this page to "Mastering Clean Code: Essential Practices for Javascript Developers". That would make it a more clear and descriptive name.

Maybe put the language version in the text at the top of the page. So instead of "JavaScript", it could say, "JavaScript (ECMAscript 2023)". That may help future readers understand the context, depending on future additions to the language.

Thank you for writing this, Mahabubur Rahman, and commenters.

Collapse
 
thenickest profile image
TheNickest • Edited

I personally like seeing developers applying clean code principles, so I hope my feedback finds you well. Your source code lacks some highlighting. Would be neat to see that next time.
Also error handling and asynchronous are not really consistent. The then/catch example appears to be ok for errors but not for asynchronous operations? It also hides the fact that there can be good reasons to choose one over the other. I agree that it can get messy if nested too much, but this is not shown in your example.

Collapse
 
mahabubr profile image
Mahabubur Rahman

thank you for share your prospective

Collapse
 
boby_tiwari_fd03ffa35b156 profile image
Boby Tiwari

Thanks, now I know how to write clean code

Collapse
 
webdevqueen profile image
Andy The Web Dev Queen

What a great post! Every developer should follow these instructions. Clean code is the best prerequisite for a great project.

Collapse
 
john_jackson_28454108c610 profile image
John Jackson

ohh, interesting