What is clean code?
That's a really controversial question and i'd like to say there are many answers to this topic but as a general idea Clean code is code that is easy to read, understand, and maintain. Following best practices and industry standards, allows you to easily write code free from bloat, redundancy, and complexity.
Let me share some of these best practices to you.
1. Naming Conventions
While it may seem basic, giving careful consideration to variable names can be crucial to making code readable and maintainable. Using descriptive and meaningful names for variables, functions, and objects can help reduce the cognitive load required to understand and work with code. Avoiding abbreviations and other shortcuts can further help improve code clarity and readability.
For example, instead of using a vague variable name like data, consider using a more descriptive name that reflects the actual data being represented, such as customerData or productList.
// ❌ Avoid this
const d = [1, 2, 3];
// ✅ Do this
const data = [1, 2, 3];
// ❌ Avoid this
function add(a) {
return a + 1;
}
// ✅ Do this
function addOne(number) {
return number + 1;
}
2. Name your functions
Try to give every function a name including closures and callbacks. Generally avoid anonymous functions, otherwise you'll have difficulties while profiling your app for example.
Likewise, debugging production issues using any monitoring tool might become challenging as you won't be able to easily find the root cause of your problem.
Meanwhile a named function will allow you to easily understand what you're looking at when checking a memory snapshot or else.
3. Use ES6 Features
ES6, the latest version of JavaScript, introduced several features that can greatly improve the cleanliness and conciseness of JavaScript code. Just to name a few, destructuring allows developers to extract values from arrays or objects more easily, arrow functions offer a more concise syntax for defining functions, and template literals enable the embedding of expressions into string without the need for concatenation or escaping characters.
Let demonstrate it with some easy code:
// const and let declarations
const PI = 3.14;
let name = 'John Doe';
// Template literals
const message = `Hello ${name}!`;
// Arrow functions
const square = (x) => x * x;
// Default parameters
const add = (a, b = 0) => a + b;
// Destructuring
const data = [1, 2, 3];
const [first, second, third] = data;
const person = {
firstName: 'John',
lastName: 'Doe'
};
const { firstName, lastName } = person;
// Spread operator
const lotus = [1, 2, 3];
const numbers = [...lotus, 4, 5, 6]; // [1, 2, 3, 4, 5, 6]
// Rest parameters
const sum = (...numbers) => numbers.reduce((total, current) => total + current, 0);
sum(...numbers) // 21
// Object literals
const firstName = 'John';
const lastName = 'Doe';
const person = { firstName, lastName };
// Class syntax
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
4. Avoid Global Variables
Global variables can pose several problems, including naming conflicts leading to reduced maintainability. To me, they should generally be avoided unless the goal is to share this variable across the app for a general purpose like env variables. To organize and manage code more effectively, consider using modules. Modules provide a way to encapsulate related code and data, minimizing the risk of naming conflicts, unexpected side effects and ultimately making it easier to maintain and update code.
// ❌ Avoid this
var counter = 0;
function incrementCounter() {
counter++;
}
// ✅ Do this
// module.js
let counter = 0;
function incrementCounter() {
counter++;
}
export { counter };
// ✅ Even better
function createCounter() {
let counter = 0;
return {
increment: function () {
counter++;
},
getCount: function () {
return counter;
}
};
}
const counter = createCounter();
counter.increment();
- Bad example : the variable counter is declared as a global variable, which can cause naming conflicts and make the code harder to maintain.
- Good example : the variable is declared inside a module before being exported, which makes it a local variable to the module thus avoiding the naming conflicts.
- Better example : the variable is encapsulated inside a function, which creates a closure. This way, the variable is not accessible from outside the function, making it truly private and avoiding any naming conflicts.
5. Keep functions small
It is important to keep your functions small and focused on a single responsibility. By breaking down complex logic into smaller functions, you can increase the readability of your code and make it easier to reason about. Smaller functions are often more reusable and easier to test which can save you time and effort in the long run. As a rule of thumb always remember that functions should only do one thing and do it well.
// ❌ Avoid this
function calculateOrderTotal(items, shippingMethod, discount) {
let subtotal = 0;
for (const item of items) {
subtotal += item.price * item.quantity;
}
let shippingCost = 0;
if (shippingMethod === 'standard') {
shippingCost = subtotal * 0.1;
} else if (shippingMethod === 'express') {
shippingCost = subtotal * 0.2;
}
let discountAmount = 0;
if (discount) {
discountAmount = subtotal * discount;
}
return subtotal + shippingCost - discountAmount;
}
// ✅ Do this
function calculateSubtotal(items) {
return items.reduce((acc, item) => acc + item.price * item.quantity, 0)
}
function calculateShippingCost(subtotal, shippingMethod) {
const shippingRates = {
standard: 0.1,
express: 0.2,
};
return Object.keys(shippingRates).reduce((shippingCost, method) => {
if (shippingMethod === method) {
shippingCost = subtotal * shippingRates[method];
}
return shippingCost;
}, 0);
}
function calculateDiscountAmount(subtotal, discount) {
let discountAmount = 0;
if (discount) {
discountAmount = subtotal * discount;
}
return discountAmount;
}
function calculateOrderTotal(items, shippingMethod, discount) {
const subtotal = calculateSubtotal(items);
const shippingCost = calculateShippingCost(subtotal, shippingMethod);
const discountAmount = calculateDiscountAmount(subtotal, discount);
return subtotal + shippingCost - discountAmount;
}
- Bad example, the function calculateOrderTotal is doing too many things. It's difficult to understand what the function does without stepping through each line.
- Good example, the code is refactored into smaller, more manageable functions. Each function has a single responsibility, making the code easier to understand, test, and maintain.
6. Use a linter
Linting is a helpful tool that can improve the quality of your code. It checks for potential errors, bugs, and coding violations to ensure that your code follows best practices and conventions. Using a linting tool like ESLint, helps you keep your code clean, maintainable, and catch errors before they cause problems in production. Here's an simple example of how you can use ESLint to improve your code:
npm install eslint --save-dev
Create a file named .eslintrc in the root of your project and add the following configuration:
// .eslintrc
{
"extends": "eslint:recommended",
"rules": {
"no-console": "off"
}
}
Now you can run ESLint on your code by using the following command:
npx eslint your-file.js
Resulting in this code update:
// ❌ Original code
const name = 'John Doe';
console.log('Hello, ' + name);
// ✅ Corrected code
const name = 'John Doe';
console.log(`Hello, ${name}`);
As annoying as it might seem at the beginning with all those red lines poping, it ensures your code is easier to understand and maintain, both for your future self and for other developers who'll work on the project, and believe me, they'll thank you for it.
7. Follow the Single Responsibility Principle
The Single Responsibility Principle (SRP) is a fundamental principle in software development that states that every module, class, or function should have a single, well-defined responsibility.
Let's take a look at an example of SRP:
class User {
constructor(name, email, password) {
this.name = name;
this.email = email;
this.password = password;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
setPassword(password) {
this.password = password;
}
sendEmail() {
// code to send an email to the user
}
}
Here, the User class has a single, well-defined responsibility: managing a user's information.
The getName and getEmail methods retrieve the user's name and email address respectively. While the setPassword method updates the user's password, and the sendEmail method sends an email to the user.
Each method does exactly what it states and they don't overlap or have multiple responsibilities.
8. Write Test Cases
When you write code, you want it to work correctly and not break unexpectedly when deploying to production. That's where test cases come in. Think of them as a way to double-check your work and make sure your code does what it's supposed to do.
When you write test cases, be sure to cover all the important parts of your code. That includes the normal situations, but also the tricky edge cases where things might not work as intended. You'll also want to test what happens when something goes wrong, like when a user enters the wrong input or when you API is down for whatever reason.
By taking the time to write good test cases, you can catch mistakes and bugs early in the process. That means you can fix them before they become bigger problems down the line. Plus, having good test cases means you can make changes and updates to your code with more confidence, knowing that you're not introducing new problems or breaking what's currently working.
9. Use Descriptive Error Messages
While programming, we all make mistakes that's fine. However, when we do, we need to debug and preferably fast. That's where error messages come in, helping us to quickly identify and fix these issues.
Therefore, it's essential to create error messages that are clear and informative, explaining the cause of the error and providing guidance on how to fix it can save us a lot of time and frustration in the long run by helping us quickly diagnose and address any problems that arise in our code. So, always take the time to craft descriptive error messages that can make the debugging process more manageable and less stressful.
try {
// code that may throw an error
if (someCondition) {
throw new Error("Invalid argument: someCondition must be true");
}
} catch (error) {
console.error(error.message);
}
In this example, we use a try-catch block to handle errors. If the code inside the try block throws an error, the error message is caught and logged to the console using console.error().
The error message includes the description "Invalid argument: someCondition must be true", which provides enough information to help the developer locate and understand the cause of the error before fixing it.
As a side benefit it might allow you to find the bugging line in your code even if you didn't share your sourcemaps to your monitoring tool.
Additionally, if our code is being used by others, providing descriptive error messages can improve the overall user experience by helping users to quickly understand what went wrong which provides a better user experience, reduces frustration and confusion (but don't provide too much info to the frontend as it might also help unfriendly user to take advantage of your app).
As a side note, don't hesitate to use multiple try catch blocs when needed otherwise you might find the error to be not specific enough, thus pointless.
10. Refactor Regularly
Refactoring is the process of improving the internal structure of your code without changing its behavior.
As your code grows and changes over time, it can become more complex and harder to maintain.
Refactoring can help improve the performance of your code, reduce bugs and errors, and make it easier for you and other developers to understand and modify the code in the future.
Let's pretend you have a piece of code that has become difficult to manage and understand:
function calculateSum(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}
This code calculates the sum of an array of numbers. Over time, however, the code may become harder to maintain as the number of responsibilities or the complexity of the code increases.
To address this, we can refactor the code, here using a simple reduce :
function calculateSum(numbers) {
return numbers.reduce((sum, number) => sum + number, 0);
}
Conclusion
Writing clean and readable code is essential to produce quality software. Following the best practices mentioned above, such as keeping functions short and focused on a single responsibility, using descriptive variable and function names, and using error messages can make your code more maintainable, scalable, and easier to understand.
It is important to remember that writing clean code is not a one-time task, but a continuous process that requires practice and discipline. As you practice writing clean code, you will find it easier to maintain and build upon your existing codebase, which can save you time and effort in the long run. By continuously improving the quality of your code, you can produce software that is reliable, efficient, and easy to work with.
Keep the good work and happy coding! 🚀
Top comments (13)
Mostly agree with your points, with 2 caveats:
For complex, multiline methods I'd usually abstract it out though.
Great addition thanks!
I have to say i also use abbreviations in for loop for example where I believe it's descriptive enough but i generally try to avoid it for variables or function naming to make sure my fellows or even my future self will be able to read through it with ease.
As for functions it's mostly fine indeed but in general i want people to consider this: why take the risk to deteriorate debugging experience just to skip a few characters?
ES6 is the latest version of Ecmascript? Dude, it's 8 years old. The latest version is 2023.
Chatgpt wrote this or what?
ES6 has become a catch-all for all later updates. This is likely because it was the major update and people needed a term to differentiate when searching for questions/answers/solutions/examples/everything to ensure they didn't get older results.
It's not right, but is is very common.
Indeed ES6 represents the biggest change when a lot of people would consider 'modern' javascript was born. That was the sea change. It's also the point at which versioning became annual. 'ES6' is kinda shorthand for the post ES6 era.
No i wrote that, but you're right it's probably not the most appropriate term.
To me, ES6 is a label that represents the new era in JavaScript, at which point every new version would build upon it.
I mean, out the top of your head could you mention what ES2022 or even ES2023 introduced?
I couldn't and that's probably normal because it wasn't has disruptive, "just" a few features added.
Naming Conventions: The rules for naming in the code are pretty much drilled into everybody's head. What bothers me when I read code, is when the meaning drifts from the original name. One obvious case is when you change the name of a control, like a checkbox, but the variable name stays the same. Sometimes, the role of the control drifts farther away from the original name, but nobody changes the variable name. I've even seen variables or functions named the opposite of what they do. And, you know, doing the programming, sometimes you just don't pay attention. Well, new readers of that code do. This is the kind of thing that makes code hard to read, if you have to constantly re-map names in your head.
So, when the role of a variable or function changes, change the name, too. Always global search and replace to make sure you change every last instance! Including in comments.
This is good to do in refactoring. I usually gravitate towards the name on the UI, if any.
Good point indeed, that's why i wanted to mention it. It's obvious at first but on a daily basis you quickly realize that it deserves to be reminded!
Very helpful! Thanks!
Nice work 👍
Helpful post
Awesome 👍
Nice 👍