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.
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;
}
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.
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}`;
}
}
Global variables can pose several problems, including naming conflicts leading to reduced maintenability. To me, they should generally be avoided unless the goal is to share this variable accross 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();
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;
}
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.
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.
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.
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.
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);
}
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! 🚀
Honestly i am no one, i've just been coding for 3 years now and i like to document every solutions to every problem i encounter. Extracting as much code snippets and tutorials i can so that if i ever need it again i just have to pop back here and get a quick refresher.
Feel free to me through this learning journey by providing any feedback and if you wanna support me: