5 Best Practices for Clean 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.

Looking to improve your skills? Check out our interactive course to master JavaScript in 5 hours.
JavaScript Course

#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?

// 🔴 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
Copied to clipboard!

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:

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
Copied to clipboard!

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:

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
Copied to clipboard!

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.

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
Copied to clipboard!

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

(O)pen-closed principle

(L)iskov substitution principle

(I)nterface segregation principle

(D)ependency inversion principle

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:

// 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
Copied to clipboard!

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:

// 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
Copied to clipboard!

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


#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.

// 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
Copied to clipboard!

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

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.

// 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
Copied to clipboard!

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.

// 🔴 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
Copied to clipboard!

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.

// 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
Copied to clipboard!

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.

// 🔴 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
Copied to clipboard!

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

// 🔴 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
Copied to clipboard!

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.

// 🔴 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
Copied to clipboard!

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

// 🔴 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
Copied to clipboard!

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.

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

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:

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

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

// ✅ PREFER
if (user.isAdmin) { ... }
comment.jsNo comment is needed if the intent is clear
Copied to clipboard!

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.

// ⚠️ 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
Copied to clipboard!

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:

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:

    .
    .
    .
} // End of if statement

// ---------- Beginning of authentication ----------
if (user.isAuthenticated) {
    .
    .
    .
comment.js
Copied to clipboard!

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
JavaScript Course Dashboard

Tired of looking for tutorials?

You are not alone. Webtips has more than 400 tutorials which would take roughly 75 hours to read.

Check out our interactive course to master JavaScript in less than 5 hours.

Learn More

📚 Get access to exclusive content

Want to get access to exclusive content? Support webtips with the price of a coffee to get access to tips, checklists, cheatsheets, and much more. ☕

Get access Support us
Read more on
Ezoicreport this ad
🎉 Thank you for subscribing to our newsletter. x