Best Practices for Writing Readable and Maintainable JavaScript Code
Striving for the Three Rs in Software Development: Refactorability, Reusability, and Readability
Good code is easy to understand and maintain, embodying the three Rs of Software Architecture: Refactorability, Reusability, and Readability.
While these principles have always been crucial, they are even more important today. Modern products are often developed in multiple ways to cater to diverse audiences, devices, and platforms. This makes sharing and reusing components between projects essential.
Code that isn't easy to isolate, reuse, and understand won't be adopted by other projects.
To ensure your code meets these standards, consider publishing components to Bit. If your code is well-structured, you'll find it straightforward to publish and reuse components in other projects.
Variables
Meaningful Names
//BAD
cosnt ddmmyyyy = new Date();//GOOD
const date = new Date();
Searchable Variable Names
We spend more time reading code than we do writing code. That is why it’s important that it is readable and searchable. If you see a value and have no idea what it does or is supposed to do, that would be confusing on the reader’s end.
//BAD
//Reader would have no clue on what 86400000 is
setTimeout(randomFunction, 86400000);//GOOD
// Declare them as capitalized named constants.
const MILLISECONDS_IN_A_DAY = 86_400_000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
Avoid Mental Mapping
Don’t force people to memorize the variable context. Variables should be understood even when the reader has not managed to follow the whole history of how they came to be.
// BAD
const names = ["John", "Jane", "Joseph"];
names.forEach(v => {
doStuff();
doSomethingExtra();
// ...
// ...
// ...
// What is this 'v' for?
dispatch(v);
});// GOOD
const names = ["John", "Jane", "Joseph"];
names.forEach(name => {
doStuff();
doSomethingExtra();
// ...
// ...
// ...
// 'name' makes sense now
dispatch(name);
});
Do Not Add Unwanted Context
If your class or object name tells you what it is, do not include that in the variable name.
// BAD
const Book = {
bookName: "Programming with JavaScript",
bookPublisher: "Penguin",
bookColour: "Yellow"
};
function wrapBook(book) {
book.bookColour = "Brown";
}// GOOD
const Book = {
name: "Programming with JavaScript",
publisher: "Penguin",
colour: "Yellow"
};
function wrapBook(book) {
book.colour = "Brown";
}
Use Default Arguments
Instead of using the short-circuit approach, we provide default arguments to variables to end up with a much cleaner output.
// BAD
function addEmployeeType(type){
const employeeType = type || "intern";
//............
}// GOOD
function addEmployeeType(type = "intern"){
//............
}
Use Strong Type Checks
Use ===
instead of ==
. This would help you avoid all sorts of unnecessary problems later on. If not handled properly, it can dramatically affect the program logic.
0 == false // true
0 === false // false
2 == "2" // true
2 === "2" // false
Functions
Use Descriptive Names That Speak for Themselves
Considering functions that represent a certain behaviour, a function name should be a verb or a phrase fully exposing the intent behind it as well as the intent of the arguments. Their name should say what they do.
//BADfunction sMail(user){
//........
}//GOODfunction sendEmail(emailAddress){
//.........
}
Minimal Function Arguments
Ideally, you should avoid a long number of arguments. Limiting the number of function parameters would help it easier to test your function.
One or two arguments is the ideal case, and three should be avoided if possible. Anything more than that should be consolidated. Usually, if you have more than two arguments, then your function is trying to do too much. In cases where it’s not, most of the time, a higher-level object will suffice as an argument.
//BAD
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);//GOOD
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
Functions Should Only Do One Thing
This is one of the most important rules in software engineering. When your function does more than one thing, it is harder to test, compose and reason about. When you isolate a function to just one action, it can be refactored easily, and your code will read much much cleaner.
//BAD
function notifyListeners(listeners) {
listeners.forEach(listener => {
const listenerRecord = database.lookup(listener);
if (listenerRecord.isActive()) {
notify(listener);
}
});
}//GOOD
function notifyActiveListeners(listeners) {
listeners.filter(isListenerActive).forEach(notify);
}
function isListenerActive(listener) {
const listenerRecord = database.lookup(listener);
return listenerRecord.isActive();
}
Remove Duplicate Code
You should do your best to avoid code duplication. Writing the same code more than once is not only wasteful when writing it the first time but even more so when trying to maintain it. Instead of having one change affect all relevant modules, you have to find all duplicate modules and repeat that change.
Oftentimes, duplication in code happens because two or more modules have slight differences that make it because you have two or more slightly different things that share much in common.
Small differences force you to have very similar modules. Removing duplicate code means creating an abstraction that can handle this set of different things with just one function/module/class.
//BAD
function showDeveloperList(developers) {
developers.forEach(developer => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}//GOOD
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
Do Not Pollute The Globals
Polluting globals is a bad practice in JavaScript because you could clash with another library, and the user of your API would be none-the-wiser until they get an exception in production. For example, if you wanted to extend JavaScript’s native Array
method to have a diff
method that would show the difference between two arrays. You can write your method to Array.prototype
, but it can clash with another library that has tried to call the same diff
method to implement another feature.
This is why it would be much better to just use ES2015/ES6 classes and simply extend the Array
global.
//BAD
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};//GOOD
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
Always“use strict” On
Chances are that if you are using any library/framework or compiler for your Javascript, “use strict” is on, but just in case you are not, remember to add it to the file and to the functions. It will make sure you get errors that would happen silently if you don’t include it.
Use Function expressions instead of Function Declarations
Unless you want to take advantage of function behaviour and properties, you prefer function expressions. Function declarations are hoisted, and although they can be useful sometimes, avoid them as they introduce weird behaviour to the code, and it is not always obvious what’s happening. Try to make it clear where the function you are using comes from and that they come before you use them to avoid weird access.
Stop using “var”!
Declarations with “var” are also hoisted, which makes var declarations accessible before where the declaration happens, which is weird, non-obvious behaviour.
Use “const” and immutability as much as possible
Prefer immutability as much as possible. Constantly changing data and passing it around can make it hard to track bugs and the changes themselves. Work on data copies and avoid side effects.
Prefer Class over Constructor Functions
Although the constructor function allows you to do some very nice stuff, if you find yourself reaching out for its prototype is a sign you need to use “class” which are supported pretty much anywhere. It is cleaner and something people are more likely to understand.
Always use “===”
The triple equal checks for value and type and it is something you always want to do. Make it a habit to always triple-check and avoid undesirable effects.
Avoid Global Variables
Avoid creating things in global objects unless you are creating a library/framework. Global property names may collide with third parties or something a colleague also introduced and are hard to debug.
Organize your declarations
Be consistent with the way you declare things. Put all your declarations on top, starting with the constants and moving down to the variables. Make constants all uppercase to indicate they are constants, which will prevent developers from trying to change them.
Don’t initialize things with “undefined”
Something is “undefined” when it lacks value. Let’s agree that assigning “no value” as a “value” for something is a pretty weird concept, right? Since Javascript already makes things “undefined”, how can you tell whether something is undefined because of you or Javascript? It makes it hard to debug why things are “undefined” so prefer setting things to “null” instead.
Always initialize your declarations
For the same reason, you should not give “undefined” as a value to declarations; you should not leave them without a value because they are “undefined” by default.
Lint your code and have a consistent style
Linting your code is the best way to ensure a consistent look and feel of your code and make sure people don’t do weird things to it as well. It puts everyone on the same page.
Use Typescript
Typescript can help you a lot in delivering better code. It will need some getting used to if you have never tried a type system, but it pays off in the long run.
Don’t be lazy when naming things
Always put some effort into naming things. If it is hard to name, you probably gave it extra responsibility or do not understand what it is. Give it at least a 3 letter name with meaning.
Avoid unnecessary declarations
Some declarations can be avoided altogether, so only declare when it is strictly necessary. Too many declarations may hint at a lack of proper code design or declaration consideration
Use default values when possible
Having defaults is more elegant than throwing errors because something was not provided. If you really want to catch not provided values, you can check my article on 25 javascript solutions, where I share a way to make things required that throw an error if no value is provided.
Always have a default case for switch statements
Don’t leave your switch statements without a default case because something can go wrong, and you want to make sure you catch it.
Never use “eval”
Never! It is not necessary.
Add meaningful comments for nonobvious things
Only add comments when you did something not common, weird, or require context to be understood. Also, add comments to things that are a hack or may require improvements/fixing later on so the next person knows why. Add comments in your third parties’ modules and modules in your codebase to explain the architecture and the intention behind things.
Keep ternaries simple
In the worst-case scenario, you have two nested ternaries. Anything longer should be an if statement or switch for readability and easy-to-debug reasons.
Prefer promises over callbacks
Promises are easy to use, and anything with a callback can be “promised”. Callbacks are synchronous, and with promises and async…await, you get to do things asynchronous, which helps speed up your code, especially because Javascript is single-threaded.
For loops > .forEach, sometimes
Don’t change things into an array just so you can “.forEach” it. You are adding an extra process to a slow alternative. For loops are faster and allow you to use the “continue” and “break” keywords to control the looping.
“for…in” and “for…of”
The for-in and for-of loops are very powerful ways to loop. The “for-of” loop lets you go over the values of the array, strings, Map, Set, etc. No need to change something into an array to use .forEach. I would avoid the “for-in” for looping as it is the slowest one and iterates over prototype keys.
Optimize for loops
If you ever need to make for-loops faster try storing the properties of “length” for example and avoid accessing it on every iteration. Check this amazing article explaining loop optimization in more detail.
Always “try…catch” JSON methods
Don’t trust things passed to JSON methods “.stringify” and “.parse”. Try to catch them to make sure they don’t fail and break your code.
Prefer template strings
It is that simple. Template strings allow you to inject values into the string and they keep the format that can come in handy.
Avoid nesting or chaining loops
When you chain iteration method or nest loops you are increasing the complexity of the code which may slow things down later on or as your data grows. Even though some operations may require it, always assess your looping strategy to ensure you don’t have unnecessary loops or loops that can be combined together.
Avoid Weird, Unreadable hacks
They are all over the internet because people find them “cool”. They are usually weird, non-conventional, and non-obvious when you look at them. It is always best to follow the guidelines of the tool you are using to ensure proper performance. Hacking should be that last alternative.
Prefer the“rest” operator over“arguments”
The “rest” operator will work with arrow functions where “arguments” are not available. Stick to one way to access your function arguments.
Prefer “globalThis” for global access
Let the Javascript handle the rest, and make sure that your code will work whether it is inside a Web Worker or Backend Node.
Understand Javascript but Build with Libraries and Frameworks
I recommend investing time in understanding the Javascript language itself but build with powerful tools like React and Angular to avoid common mistakes. Make sure you follow their guidelines since these tools already guard for common mistakes and employ best practices.
Add semicolons, always!
You may be surprised to find out that you can get away with not putting a semicolon in the Javascript code. Know that the compiler adds them and tools like Babel may easily misread your code and cause a bug to make to production. Always add semicolons!
Readable > Performance unless you need Performance
There are ways to get more performance by doing things that are often hard to read, but unless you are desperate for performance at the code level (which is rare), make it readable.
Watch out for “undefined” and “null” with the “??” operator
The nullish coalescing operator makes sure that null and undefined values are not picked, which is perfect for cases where you want to ensure that there is a value or fallback to a default value.
Never trust data you don’t create
Whenever you are dealing with data coming from a user or from an API you don’t know, make sure it is of the right type and in a format that you can work with before you do any operation on it.
Use regex when extracting and looking for things in Strings
Regex is super powerful and fun. Avoid weird manipulation, like looking for indexes and grabbing things. Regex allows you to look for complex patterns and
IIFEand small utility libraries
IIFE is an excellent way to execute things as early as possible, which you can use to set up some stuff before the rest of the code starts running. You can also use it to initialize small libraries with a simple API that allows you to encapsulate some complex logic and expose an object you can use to interact with, similar to how jQuery is built.
Avoid repeating yourself with utilities
Always turn things you do repeatedly into small generic functions that you can reuse later on. As a developer, you should not be repeating things, and small functions make them easy to test and reuse.
Add Unit Tests
As a developer, I often found bugs when I started adding unit tests. Tests are the ultimate way to ensure that your code is as error-free as possible. Jest is an excellent option to start with, but there are others out there that are also as simple to use.
Although this is a vast topic, I have limited it to only variables and functions to keep the post short. I have only included a fraction of what you can do to write clean code. I highly suggest you read “Clean Coder” by Robert C. Martin.