Javascript Best Practices for Code Clarity and Maintainability

Olagunjuadewale
9 min readDec 24, 2023

--

Creating code that functions is just the beginning; the true goal is to produce code that is easily comprehensible, reusable, and adaptable by others. The significance of writing clean code lies in the fact that, in a typical professional setting, your audience extends beyond yourself and the machine. Instead, you are catering to a community of developers who will engage with, modify, and expand upon your work.

This article delves into the realm of crafting clean JavaScript code, emphasizing practices that are not tied to a specific framework. While the examples provided are JavaScript-centric, the principles discussed can be widely applicable to almost any programming language. Primarily derived from Robert C. Martin’s influential book, “Clean Code,” the concepts outlined herein are recommendations rather than strict rules, offering a flexible approach to code cleanliness.

The Art of Choosing Descriptive Names

Variable names should be descriptive, and it’s a common practice in JavaScript to use Camel Case (camelCase) for most variable names

// Avoid
var bar = "email@example.com"
var data = "john"
var foo = 23
var mmm = true

function change() {
// ... do something
}

function confirm(user) {
// ... do something
}


// Prefer
var email= "email@example.com"
var firstName = "john"
var age = 23
var isOnline = true

function changeAppTheme() {
// ... do something
}

function confirmAllTerms(user) {
// ... do something
}

Be aware that boolean names typically respond to specific inquiries, such as:

isActive
isOnline
didSubscribe
hasLinkedAccount

Minimizing Unnecessary Contexts in Programming

Avoid incorporating redundant context into variable names if the context is already conveyed by the encompassing object or class.

// Avoid
const userDetails = {
userId: 1,
userEmail: "email@example.com",
userFirstName: "John",
userLastName: "Doe",
userAge: 23,
};

userDetails.userId;

// Prefer
const user = {
id: 1,
email: "email@example.com",
firstName: "John",
lastName: "Doe",
age: 23,
};

user.id;

Eradicating Hardcoded Values

Instead of inserting fixed values, ensure to define significant and easily searchable constants. It’s worth noting that global constants can be formatted, such as in the case of a User Authentication Token (USER_AUTHENTICATION_TOKEN).

// Avoid
setTimeout(clearSessionData, 900000);

// Prefer
const SESSION_DURATION = 15 * 60 * 1000;

setTimeout(clearSessionData, SESSION_DURATION);

Harnessing the Power of Default Arguments in Your Code

Utilizing default arguments offers a cleaner approach compared to employing short-circuiting or inserting additional conditional statements within the function body. It’s crucial to note, however, that short-circuiting is effective for all values deemed "falsy" including false, null, undefined, '' , "", 0, and NAN whereas default arguments exclusively substitute undefined.

// Avoid
function logFilesInDirectory(dir) {
const directory = dir || "./";
// ...
}

// Prefer
function logFilesInDirectory(dir = "./") {
// ...
}

Restrict the number of Arguments

While this guideline may spark debate, it advocates for functions to ideally possess 0, 1, or 2 arguments. Introducing a third argument is considered excessive, and surpassing that threshold suggests one of two scenarios:

  1. The function is performing multiple tasks and should undergo a division for improved clarity.
  2. The data supplied to the function is interconnected and can be encapsulated within a dedicated data structure.
// Avoid
function sendPushNotification(title, message, image, isSilent, delayMs) {
// ...
}

sendPushNotification("New Message", "...", "http://...", false, 1000);

// Prefer
function sendPushNotification({ title, message, image, isSilent, delayMs }) {
// ...
}

const notificationConfig = {
title: "New Message",
message: "...",
image: "http://...",
isSilent: false,
delayMs: 1000,
};

sendPushNotification(notificationConfig);

Refrain from incorporating multiple actions within a single function

A function ought to perform a singular task at a time. Adhering to this principle diminishes the function’s size and complexity, leading to simplified testing, debugging, and refactoring processes. The line count within a function serves as a robust indicator, signaling potential concerns if it exceeds a certain threshold. Ideally, aim for a code length of less than 20–30 lines to maintain clarity and focus within the function.

// Avoid
function pingUsers(users) {
users.forEach((user) => {
const userRecord = database.lookup(user);
if (!userRecord.isActive()) {
ping(user);
}
});
}

// Prefer
function pingInactiveUsers(users) {
users.filter(!isUserActive).forEach(ping);
}

function isUserActive(user) {
const userRecord = database.lookup(user);
return userRecord.isActive();
}

Refrain from utilizing flags as Arguments

If there’s a flag within the arguments, it implies that the function can be further streamlined.

// Avoid
function createFile(name, isPublic) {
if (isPublic) {
fs.create(`./public/${name}`);
} else {
fs.create(name);
}
}

// Prefer
function createFile(name) {
fs.create(name);
}

function createPublicFile(name) {
createFile(`./public/${name}`);
}

Avoid redundancy; adhere to the principle of not repeating yourself (DRY)

Replicating code is always a red flag. When you duplicate code, any alterations to the logic necessitate updates in multiple locations.

// Avoid
function renderCarsList(cars) {
cars.forEach((car) => {
const price = car.getPrice();
const make = car.getMake();
const brand = car.getBrand();
const nbOfDoors = car.getNbOfDoors();

render({ price, make, brand, nbOfDoors });
});
}

function renderMotorcyclesList(motorcycles) {
motorcycles.forEach((motorcycle) => {
const price = motorcycle.getPrice();
const make = motorcycle.getMake();
const brand = motorcycle.getBrand();
const seatHeight = motorcycle.getSeatHeight();

render({ price, make, brand, seatHeight });
});
}

// Prefer
function renderVehiclesList(vehicles) {
vehicles.forEach((vehicle) => {
const price = vehicle.getPrice();
const make = vehicle.getMake();
const brand = vehicle.getBrand();

const data = { price, make, brand };

switch (vehicle.type) {
case "car":
data.nbOfDoors = vehicle.getNbOfDoors();
break;
case "motorcycle":
data.seatHeight = vehicle.getSeatHeight();
break;
}

render(data);
});
}

Steer clear of introducing side effects.

In JavaScript, it is advisable to prioritize functional patterns over imperative ones. Put simply, strive to keep functions pure unless there is a specific need for an alternative approach. Side effects have the potential to alter shared states and resources, leading to unintended consequences. To manage side effects effectively, centralize them; if there’s a requirement to mutate a global value or modify a file, establish a singular and dedicated service for that purpose.

// Avoid
let date = "21-8-2021";

function splitIntoDayMonthYear() {
date = date.split("-");
}

splitIntoDayMonthYear();

// Another function could be expecting date as a string
console.log(date); // ['21', '8', '2021'];

// Prefer
function splitIntoDayMonthYear(date) {
return date.split("-");
}

const date = "21-8-2021";
const newDate = splitIntoDayMonthYear(date);

// Original vlaue is intact
console.log(date); // '21-8-2021';
console.log(newDate); // ['21', '8', '2021'];

Additionally, when a mutable value is provided as an argument to a function, it is advisable to generate and return a new mutated clone of the value instead of directly altering and returning the original value

// Avoid
function enrollStudentInCourse(course, student) {
course.push({ student, enrollmentDate: Date.now() });
}

// Prefer
function enrollStudentInCourse(course, student) {
return [...course, { student, enrollmentDate: Date.now() }];
}

Employ conditionals with non-negative logic.

Adopt a programming approach where conditionals are structured with a focus on non-negative logic, emphasizing clarity and simplicity in decision-making processes

// Avoid
function isUserNotVerified(user) {
// ...
}

if (!isUserNotVerified(user)) {
// ...
}

// Prefer
function isUserVerified(user) {
// ...
}

if (isUserVerified(user)) {
// ...
}

Opt for shorthand expressions whenever feasible.

Embrace brevity and efficiency by opting for shorthand expressions whenever they are viable. Shorter, more concise code not only enhances readability but also contributes to a more streamlined and efficient programming approach. Choosing shorthand expressions when appropriate can lead to clearer, more maintainable code, fostering a coding style that prioritizes simplicity and effectiveness.

// Avoid
if (isActive === true) {
// ...
}

if (firstName !== "" && firstName !== null && firstName !== undefined) {
// ...
}

const isUserEligible = user.isVerified() && user.didSubscribe() ? true : false;

// Prefer
if (isActive) {
// ...
}

if (!!firstName) {
// ...
}

const isUserEligible = user.isVerified() && user.didSubscribe();

Minimize the use of branching and aim for early returns.

Implementing early returns enhances the linearity, readability, and simplicity of your code.

// Avoid
function addUserService(db, user) {
if (!db) {
if (!db.isConnected()) {
if (!user) {
return db.insert("users", user);
} else {
throw new Error("No user");
}
} else {
throw new Error("No database connection");
}
} else {
throw new Error("No database");
}
}

// Prefer
function addUserService(db, user) {
if (!db) throw new Error("No database");
if (!db.isConnected()) throw new Error("No database connection");
if (!user) throw new Error("No user");

return db.insert("users", user);
}

Prefer using object literals or maps instead of switch statements.

In cases where applicable, utilizing object or map indexing will streamline the code and enhance performance.

// Avoid
const getColorByStatus = (status) => {
switch (status) {
case "success":
return "green";
case "failure":
return "red";
case "warning":
return "yellow";
case "loading":
default:
return "blue";
}
};

// Prefer
const statusColors = {
success: "green",
failure: "red",
warning: "yellow",
loading: "blue",
};

const getColorByStatus = (status) => statusColors[status] || "blue";

Opt for the utilization of optional chaining

const user = {
email: "email@example.com",
billing: {
iban: "...",
swift: "...",
address: {
street: "Some Street Name",
state: "CA",
},
},
};

// Avoid
const email = (user && user.email) || "N/A";
const street =
(user &&
user.billing &&
user.billing.address &&
user.billing.address.street) ||
"N/A";
const state =
(user &&
user.billing &&
user.billing.address &&
user.billing.address.state) ||
"N/A";

// Prefer
const email = user?.email || "N/A";
const street = user?.billing?.address?.street || "N/A";
const state = user?.billing?.address?.state || "N/A";

Minimize the use of callbacks.

Callbacks can lead to intricate and nested code structures. ES6 introduces Promises, enabling the chaining of callbacks for more organized code. However, ECMAScript 2017 presents the “Async/Await” syntax as a potentially neater alternative, imposing a more linear flow to the code.

// Avoid
getUser(function (err, user) {
getProfile(user, function (err, profile) {
getAccount(profile, function (err, account) {
getReports(account, function (err, reports) {
sendStatistics(reports, function (err) {
console.error(err);
});
});
});
});
});

// Prefer
getUser()
.then(getProfile)
.then(getAccount)
.then(getReports)
.then(sendStatistics)
.catch((err) => console.error(err));

// or using Async/Await

async function sendUserStatistics() {
try {
const user = await getUser();
const profile = await getProfile(user);
const account = await getAccount(profile);
const reports = await getReports(account);
return sendStatistics(reports);
} catch (e) {
console.error(err);
}
}

Manage thrown errors and rejected promises.

There’s no requirement to elaborate on the significance of this rule. Investing time upfront in accurate error handling significantly diminishes the chances of troubleshooting bugs later, particularly in the production phase of your code.

// Avoid
try {
// Possible erronous code
} catch (e) {
console.log(e);
}

// Prefer
try {
// Possible erronous code
} catch (e) {
// Follow the most applicable (or all):
// 1- More suitable than console.log
console.error(e);

// 2- Notify user if applicable
alertUserOfError(e);

// 3- Report to server
reportErrorToServer(e);

// 4- Use a custom error handler
throw new CustomError(e);
}

Comment exclusively on the business logic.

// Avoid
function generateHash(str) {
// Hash variable
let hash = 0;

// Get the length of the string
let length = str.length;

// If the string is empty return
if (!length) {
return hash;
}

// Loop through every character in the string
for (let i = 0; i < length; i++) {
// Get character code.
const char = str.charCodeAt(i);

// Make the hash
hash = (hash << 5) - hash + char;

// Convert to 32-bit integer
hash &= hash;
}
}

// Prefer
function generateHash(str) {
let hash = 0;
let length = str.length;
if (!length) {
return hash;
}

for (let i = 0; i < length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}

Utilize version control systems.

Retaining commented code or documenting comments in the source code is unnecessary since version control adequately addresses this aspect.

// Avoid
/**
* 2021-7-21: Fixed corner case
* 2021-7-15: Improved performance
* 2021-7-10: Handled mutliple user types
*/
function generateCanonicalLink(user) {
// const session = getUserSession(user)
const session = user.getSession();
// ...
}

// Prefer
function generateCanonicalLink(user) {
const session = user.getSession();
// ...
}

Provide documentation whenever feasible.

Documentation plays a pivotal role in enhancing the quality and reliability of code. It functions as a comprehensive user manual for your codebase, ensuring that anyone can grasp all facets of your code.

/**  
* Returns x raised to the n-th power.
*
* @param {number} x The number to raise.
* @param {number} n The power, should be a natural number.
* @return {number} x raised to the n-th power.
*/
function pow(x, n) {
// ...
}

This article briefly covered essential steps for enhancing your JavaScript code with the latest syntax. It’s worth noting that many of these principles can be extended and implemented across various programming languages. While adopting these practices may require some time, particularly in larger codebases, the investment ensures that your code remains readable, scalable, and easily refactored in the long run.

--

--