5 Best Practices for Clean JavaScript

5 Best Practices for Clean JavaScript

How to make your code self-documenting
Ferenc AlmasiLast updated 2021 November 11 • Read time 15 min read
We often overlook the simplest principle when it comes to coding: maintaining a clean and readable code. Learn how to make your code self-documenting
  • twitter
  • facebook
JavaScript

When writing our apps, we often tend to overlook the simplest principle when it comes to coding: maintaining a clean and readable source code. You probably heard the phrase of spaghetti code before. Meaning the codebase is hard to maintain, read, or even decipher. Even though the application could work just fine, adding new requirements and keeping support can be equal to hell. And if it comes to bug fixing, time spent searching through the code will grow exponentially.

In this article, I’ve collected five best practices for keeping your JavaScript code as clean as possible. Some of them may sound common sense, but if we do not pay close attention, we may as well forget to use them. So let’s start with some core principles.


#1: Principles

So what is clean code anyway? The answer may differ from person to person but for me, clean code is self-documenting. The intent is clear from first sight, different responsibilities are well separated. You can almost read code like natural language and this means you don’t have to use comments as a crutch. Always favor code instead.

There are some common coding principles which can help us achieve that:

KISS

KISS means “keep it simple, stupid” and is originating from the U.S. Navy. The principle states that your code will work best if kept simple. Unnecessary complexity will only decrease readability and makes your code hard to take in. Therefore, you should always strive for simplicity. This doesn’t necessarily mean you should always use the shorter solution, however. You may be able to write a nested if-else statement in one line. But the question is: Should you?

Copied to clipboard! Playground
// 🔴 DON'T
width = container > 960 ? (growWithContainer ? (container * .8) : 960) : container;

// ✅ DO
if (container > 960) {
    if (growWithContainer) {
        width = container * .8;
    } else {
        width = 960;
    }
} else {
    width = container;
}
ifs.js

DRY

DRY means “don’t repeat yourself”. This may sound common sense but it’s not always so obvious. Take a look at the following code example:

Copied to clipboard! Playground
settings.update(user.id, 'name', name).then(() => {
    user.name = name;
    console.log('Username updated...');
});

settings.update(user.id, 'password', password).then(() => {
    user.password = password;
    console.log('Password updated...');
});

settings.update(user.id, 'address', address).then(() => {
    user.address = address;
    console.log('Address updated...');
});
dry.js

Different things are happening for each call with different callbacks. Yet if you look at this from a distance, you will see repetition. The principle states that we should try to avoid redundancy. The above code can be simplified like this:

Copied to clipboard! Playground
settings = {
    update(id, key, value) {
        ...
        // Extending the update function with setting the user's property
        user[key] = value;
    }
}

settings.update(user.id, 'name', name).then(() => console.log('Username updated...'));
settings.update(user.id, 'password', password).then(() => console.log('Password updated...'));
settings.update(user.id, 'address', address).then(() => console.log('Address updated...'));
dry.js

Since all update function updates the user object on the frontend, we can outsource that line into the function itself. Another example would be the use of variables that are likely to change like class names. Instead of using them scattered throughout your code, try to store them in one place so things can be easily reconfigured if needed.

Copied to clipboard! Playground
const widget = {
    config: {
        previousButton: 'button-prev',
        nextButton: 'button-next'
    },
    
    init() {
        this.previousButton = document.querySelector(this.config.previousButton);
        this.nextButton = document.querySelector(this.config.nextButton);
    }
    
    handleNavigation() {
        this.previousButton.addEventListener('click', () => { ... });
        this.nextButton.addEventListener('click', () => { ... });
    }
}
config.js

TED

TED means “terse, expressive and does one thing”. With TED, we are aiming to keep the signal to noise ratio high, where the signal is the code and the noise is anything that is trying to make our code messy. Just like duplications, extensive comments, high cyclomatic complexity, or poor naming.

It is needed to allocate some time to take care of messes, otherwise it will continue to grow and will need to be addressed later down the road as tech debt. Because of increasing complexity, bugs may likely to be introduced.

SOLID

Last but not least, the most commonly used principle, which is an acronym where each letter is a principle in itself:

(S)ingle responsibility principle

  • It states that a class or function should only have a single responsibility. A function having different behaviors only increase complexity and decrease readability.

(O)pen-closed principle

  • Classes and functions should be open for extension but closed for modification. Already existing implementation should stay intact. Unless you are refactoring.

(L)iskov substitution principle

  • It states that objects in a program should be replaceable with their instances without altering the behavior of that program.

(I)nterface segregation principle

  • This principle is fairly straightforward. It simply states that we should rather have many specific interfaces than one generic. Meaning you should not force methods to have functionality it does not use. Instead, implement from multiple interfaces if needed.

(D)ependency inversion principle

  • This principle is all about decoupling your code. It means that top-level modules should not depend on low-level ones. Or saying it another way; depend on abstractions, not on concretions.

Following the principles listed above ensures that you are one step closer to achieve a code that is robust, reliable, easy to extend, and understand.


#2: Naming Conventions

Next, we have naming conventions. Names alone can decide how readable your code is. Let’s take the very first code example which was used in KISS and rename everything:

Copied to clipboard! Playground
// With ternary
w = co > 960 ? (gwc ? (co * .8) : 960) : co;

// Without
if (co > 960) {
    if (gwc) {
        w = co * .8;
    } else {
        w = 960;
    }
} else {
    w = co;
}
ifs.js

A lot of question arises. What each of them are doing? And what are they suppose to mean?

Always try to verbalize your code. Be specific and avoid generic prefixes as well as abbreviations. As we could see from the example above, there’s no clear intent on what a variable is supposed to hold. Some example are:

Copied to clipboard! Playground
// Example for abbreviations
// 🔴 DON'T
dat.forEach(c => {
    pos.push(new google.maps.LatLng(c.latitude, c.longitude));
});

// ✅ DO
data.forEach(coordinates => {
    positions.push(new google.maps.LatLng(coordinates.latitude, coordinates.longitude));
});

// Example for generic prefixes
// 🔴 DON'T
const user = {
    userId: 1,
    userName: 'John',
    userAge: 30
};

// ✅ DO
const user = {
    id: 1,
    name: 'John',
    age: 30
};

// Example for specific names
// 🔴 DON'T
time = 5000;

makeRequest(() => { ... }, time);

// ✅ DO
requestTimeout = 5000;

makeRequest(() => { ... }, requestTimeout);
naming.js

For a more thorough guideline on naming conventions, I recommend reading through the naming cheatsheet on GitHub.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

#3: The Use of Conditionals

When it comes to booleans, there are a couple of things we can take care of to make our code more expressive. And by doing so, we reduce the mental capacity it is needed to comprehend an implementation.

Copied to clipboard! Playground
// Do not compare booleans implicitly
// 🔴 DON'T
if (loggedIn === true) { ... }

// ✅ DO
if (loggedIn) { ... }

// Use positive conditionals to ease reading
// 🔴 DON'T
if (!isNotLoggedIn) { ... }

// ✅ DO
if (loggedIn) { ... }

// If you can, assign booleans implicitly
// 🔴 DON'T
if (user.accessLevel > 2) {
    userHasPermission = true;
} else {
    userHasPermission = false;
}

// ✅ DO
userHasPermission = user.accesLevel > 2;

// Avoid using magic numbers, they lack context and therefore meaning
// 🔴 DON'T
if (credit > 499) { ... }

// ✅ DO
const secretPrice = 499;

if (credit > secretPrice) { ... }
booleans.js

Also use databases instead of hardcoded long list of ifs or switch cases for logics such as:

  • pricing information
  • complex and dynamic business rules
  • data that often changes

This way you avoid hardcoding values, make things easier to update and you write less code which in the end makes your code less error-prone.


#4: The Use of Functions

There are a couple of things when it comes to functions. First, when should you create one? Only create functions when you see code duplication, when indenting becomes an issue or when intent is unclear.

Try to use expressive names, just as for variables or booleans.

Copied to clipboard! Playground
// Use of expressive names
// 🔴 DON'T
function marker () { ... }

// ✅ DO
function displayMarkersOnMap () { ... }

// Avoid using "and", "or"
// 🔴 DON'T
function retrieveAndDisplayUser () { ... }
    
// ✅ DO
function retrieveUser() { ... }
function displayUser() { ... }
functions.js

Avoid using words in function names such as “and”, “or”. It is an indication that the function has more responsibility than it needs to. Having a single responsibility also makes it easier to understand the function. It promotes reusability and helps to avoid side effects. If your function is pure — meaning it returns the same value given the same input — it also eases testing.

When using arguments, try to aim for 0–2. If you need to use more than that, use an object instead of a list.

Copied to clipboard! Playground
// 🔴 DON'T
function createCarousel(items, numberOfItemsToShow, showNavigation, animation) {
    ...
}

createCarousel([...], 3, true, 'slide');


// ✅ DO
function createCarousel({ items, numberOfItemsToShow, showNavigation, animation }) {
    ...
}

createCarousel({
    items: [...],
    numberOfItemsToShow: 3,
    showNavigation: true,
    animation: 'slide'
});
functions.js

Also, try to avoid using flags. It’s an indication that the function does more than one thing. And if you can, provide default values instead of using short circuits.

Copied to clipboard! Playground
// Avoid using flags
// 🔴 DON'T
function login(user, logout) {
    if (logout) {
        // log user out
    } else {
        // log user in
    }
}

login(user);       // logs user in
login(user, true); // logs user out

// Avoid using short circuits
// 🔴 DON'T
function animate(type) {
    const animationType = type || 'slide';
}

// ✅ DO
function animate(type = 'slide') { ... }
functions.js

We talked about the outlines of a function, but what about the inner parts?

When writing a new function, return early and fail fast. This means having your return statements and exception handling at the top. Not only it will make your function more readable, but it also improves its efficiency.

Copied to clipboard! Playground
// 🔴 DON'T
function hasPermission(user) {
    if (user.isAdmin || permissions.includes(user)) {
        // Do our magic
        
        return true;
    } else {
        return false;
    }
}


// ✅ DO
function hasPermission(user) {
    if (!user.isAdmin || !permissions.includes(user)) {
        return false;
    }
    
    // Do your magic
    
    return true;
}
functions.js

When using loops, prefer the functional programming way such as filter, map or reduce over traditional for loops:

Copied to clipboard! Playground
// 🔴 DON'T
for (let i = 0; i < products.length; i++) {
    products[i].price = Math.floor(product.price);
}

// ✅ DO
products.map(product => {
    product.price = Math.floor(product.price);
    return product;
});
functions.js

Also when you define variables inside the function, don’t declare them all on the top. Use them just when they are needed. They are called mayfly variables.

Copied to clipboard! Playground
// 🔴 DON'T
function displayNotification() {
    const warningSign = '⚠️';
    const errorSign = '🚨';
    
    let i;
    
    if (isWarningMessage) {
        message.type(warningSign);
        message.add(warningSign);
        
        if (isFatal) {
            message.type(errorSign);
            message.add(errorSign);
            
            for (i = 0; i < 100; i++) {
                ...
            }
        }
    } else {
        ...
    }
}

// ✅ DO
function displayNotification() {
    if (isWarningMessage) {
        const warningSign = '⚠️';
      
        message.type(warningSign);
        message.add(warningSign);
        
        if (isFatal) {
            const errorSign = '🚨';
            
            message.type(errorSign);
            message.add(errorSign);
            
            for (let i = 0; i < 100; i++) {
                ...
            }
        }
    } else {
        ...
    }
}
functions.js

Lastly, when working with multiple async functions that depend on each other — instead of nesting — try to outsource the callback functions.

Copied to clipboard! Playground
// 🔴 DON'T
getUser(user, response1 => {
    getUserSettings(response1, response2 => {
        getUserMetrics(response2, response3 => {
            console.log(response3);
        });
    });
});

// ✅ DO
getUser(user)
    .then(getUserSettings)
    .then(getUserMetrics)
    .then(response => console.log(response));
functions.js

So how long your functions should be? If you need to scroll to see all of the function, you need to reconsider your choices.

According to Robert c. Martin, your functions should rarely be over 20 lines, hardly ever over 100 lines and they should take no more than 3 params. There will always be exceptions, but having too many lines of code for a single function may be the indicator that something is off.


#5: Don’t Overdo Comments, Use it Wisely

Using comments can be tempting when you are dealing with complex logic. But before trying to write an essay, think about twice if you can express the same thing using only code. Only use comments when code alone is not sufficient. Don’t comment on the obvious.

Copied to clipboard!
// 🔴 DON'T
let i = 1; // Setting i to 1;
comment.js

If you ever find yourself commenting because the intent is unclear, it is a sign that your code can be improved. Take the following as an example:

Copied to clipboard! Playground
// 🔴 DON'T
// Make sure user is an admin
if (user.accessLevel === 2) { ... }

// ✅ DO
if (user.accessLevel === admin) { ... }

// ✅ PREFER
if (user.isAdmin) { ... }
comment.js
No comment is needed if the intent is clear

Try to avoid warnings. If you need to add a warning comment, it’s a sign that you need to refactor. Instead, fix it before you commit. Also, only add a TODO marker if you must.

Copied to clipboard! Playground
// ⚠️ WARNING!!!
// Don't touch the following if statement
// This ensures that everything is working as expected
// Has been implemented as part of fixing #58123
if (user && user.isAdmin && (!user.deactivated && !user.editor) || !user.deactivated && user.isPro) {
    ...
}
comment.js

Don’t include commit hashes, pull requests, ticket numbers, or any metadata in the code that belongs to the source control. That is just additional noise which can be easily found without a comment.

Don’t ever leave commented out code, otherwise known as “zombie code” in your codebase. It is not used, therefore it serves no purpose. It reduces readability and only brings confusion:

  • Why this was commented out?
  • Was it on purpose or did someone do it accidentally?
  • Do I need to refactor this?
  • What if I uncomment it back?
  • What did this section even do?

Since deleted code is visible in source control, you can always revert back changes if needed.

Another great example of comment which indicates that refactor is needed are dividers and brace trackers:

Copied to clipboard! Playground
    .
    .
    .
} // End of if statement

// ---------- Beginning of authentication ----------
if (user.isAuthenticated) {
    .
    .
    .
comment.js

This tells us that the complexity is too high and the code may need to be broken down into pieces.

Use comments with caution. Use them for summary and examples. Use them for documentation.


Summary

If you take these advice, it will bring your read and maintainability to the next level. The next time you need to address an issue or implement a feature request, may your journey be fast and seamless.

As a last word, I would like to close this article with a great quote from Martin Fowler:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
10 Critical Performance Optimization Steps You Should Take
  • twitter
  • facebook
JavaScript
Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.