DEV Community

Cover image for Write better code by following these JavaScript best practices
dcodes
dcodes

Posted on • Originally published at dawsoncodes.com

Write better code by following these JavaScript best practices

Whether you're a seasoned developer looking for ways to refine your coding style or a beginner eager to grasp the essentials, this blog post is for you. In this comprehensive guide, we'll explore various best practices that can help you elevate your JavaScript skills to the next level.

1. Adopt a Consistent Coding Style

The first step to improving your JavaScript game is to adopt a consistent coding style. Why is this important? A consistent coding style enhances the readability and maintainability of your code. Think of it as a blueprint that provides structure and coherence to your scripts.

There are several popular coding style guides that you can follow:

Choose a guide that aligns with your programming style and stick to it throughout your projects, your projects should look like that only one person wrote the code even if many other developers worked on the same project.

2. Naming variables and functions

Next on our list is the naming convention for variables, functions, and other code structures. This practice isn't just about aesthetics or semantics, but it's also about code readability and efficient debugging.

Remember, in JavaScript, the standard is to use camel-case for variables and functions (like myVariableName), and Pascal case for classes (like MyClassName).

// ❌ Poorly named variables:
let a = 'John';
let fn = () => console.log('Hello');

// ✅ Descriptive variable names:
let firstName = 'John';
let sayHello = () => console.log('Hello');
Enter fullscreen mode Exit fullscreen mode

3. Use Shorthands but Be Cautious

Shorthands are a great way to write code faster and cleaner, but be wary as they might return surprising results. To prevent such unexpected outcomes, make sure to always check the documentation, find relevant JavaScript code examples, and test the result.

// ❌ Traditional function declaration:
function square1 (num) {
  return num * num
}
// ✅ Using arrow functions (shorthand):
const square2 = num => num * num

// ❌ Very long code:
let x

if (y) {
  x = y
} else {
  x = 'default'
}

// ✅ A more succinct way to achieve the same result using logical OR:
let x = y || 'default'
Enter fullscreen mode Exit fullscreen mode

4. Add meaningful comments to your code

Commenting on your code is like leaving breadcrumbs for your future self or other developers. It helps in understanding the flow and purpose of your code, especially when working on team projects.

However, remember to keep your comments short, and concise, and only include key information.

// ❌ Over-commenting or not commenting at all:
function convertCurrency (from, to) {
  // Conversion logic goes here
}

// ✅ Using appropriate comments:
/**
 * Converts currency from one type to another
 *
 * @description Converts one currency to another
 * @param {string} from - The currency to convert from
 * @param {string} to - The currency to convert to
 * @returns {number} - The converted amount
 * */
function convertCurrency (from, to) {
  // Conversion logic goes here
}
Enter fullscreen mode Exit fullscreen mode

In this example, the first one does not give the developer what is from and what is to , but the second example lets the programmer easily understand what are the arguments for and describes what the function does.

5. Follow the SoC principle

To keep things simple and organized, it's best not to use JavaScript for adding direct styling. This is known as separation of concerns (SoC). Instead, use the classList API to add and remove classes, and define the style rules with CSS.

This way, CSS does all the styling and JavaScript handles all of the other functionalities of your application.

This programming concept is not exclusive to JavaScript, (SoC) the Separation of concerns is a practice to separate functionalities and not mix up different technologies.

To keep it short: CSS should do CSS stuff but JavaScript should not do CSS.

// ❌ Avoid using JavaScript for styling:
let element = document.getElementById('my-element')
element.style.color = 'red'

// ✅ Changing styles by adding/removing classes:
let element = document.getElementById('my-element')
element.classList.add('my-class')
Enter fullscreen mode Exit fullscreen mode

6. Avoid global variables

When declaring local variables, use let and const to prevent overriding global variables. Both create block-scoped local variables, but the key difference is that let can be re-declared while const can't. So, use them wisely according to your specific needs.

// ❌ Using var for variable declaration:
var x = 1

// ✅ Using let and const for variable declaration:
let x = 1
const y = 2
Enter fullscreen mode Exit fullscreen mode

7. Use for...of Instead of for Loops

The for...of the statement, introduced in ECMAScript 6, is a more efficient alternative to traditional for loops. It comes with a built-in iterator, eliminating the need to define the variable and the length value. This makes your code cleaner and enhances its performance.

// ❌ Using traditional for loop:
let cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix']
for (let i = 0; i < cities.length; i++) {
  const city = cities[i]
  console.log(city)
}

// ✅ Using for of loop:
let cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix']
for (const city of cities) {
  console.log(city)
}
Enter fullscreen mode Exit fullscreen mode

8. Adhere to the Single responsibility principle

One way to adhere to the single responsibility principle is by creating helper functions for common tasks.

These functions should be context-independent so they can be called from any module, enhancing the reusability and modularity of your code.

// ❌ Doing multiple tasks in one function:
function calculateOrderTotal (order) {
  let total = 0

  for (let i = 0; i < order.items.length; i++) {
    total += order.items[i].price * order.items[i].quantity
  }

  let tax = total * 0.07

  return total + tax
}

// ✅ Doing one task in one function:
function calculateSubTotal (items) {
  let total = 0

  for (let i = 0; i < items.length; i++) {
    total += items[i].price * items[i].quantity
  }

  return total
}

function calculateTax (total) {
  return total * 0.07
}

function calculateOrderTotal (order) {
  let subTotal = calculateSubTotal(order.items)
  let tax = calculateTax(subTotal)

  return subTotal + tax
}
Enter fullscreen mode Exit fullscreen mode

9. Understand the Lack of Hoisting in Classes

Unlike functions, classes in JavaScript are not hoisted, meaning you need to declare a class before you call it. This might seem counterintuitive at first, especially if you're accustomed to function hoisting, but it's a fundamental principle that must be understood and respected when using classes in JavaScript.

// ❌ Calling a class before declaration:
const hat = new Hat('Red', 1000)
hat.show()
class Hat {
  constructor (color, price) {
    this.color = color
    this.price = price
  }
  show () {
    console.log(`This ${this.color} hat costs $${this.price}`)
  }
}

// ✅ Calling a class after declaration:
class Hat {
  constructor (color, price) {
    this.color = color
    this.price = price
  }
  show () {
    console.log(`This ${this.color} hat costs $${this.price}`)
  }
}

const hat = new Hat('Red', 1000)
Enter fullscreen mode Exit fullscreen mode

10. Avoid Mutating Function Arguments

Directly modifying the properties of an object or the values of an array passed as a function argument can lead to undesirable side effects and hard-to-trace bugs. Instead, consider returning a new object or array. This practice aligns well with the principles of functional programming where immutability is key.

// ❌ Mutating function arguments:
function updateName (user) {
  user.name = 'bob'
}

let user = { name: 'alice' }
updateName(user)
console.log(user) // { name: 'bob' }

// ✅ Avoid mutating function arguments, return new object instead:
function updateName (user) {
  return { ...user, name: 'bob' }
}

let user = { name: 'alice' }
let updatedUser = updateName(user)
console.log(user) // { name: 'alice' }
console.log(updatedUser) // { name: 'bob' }
Enter fullscreen mode Exit fullscreen mode

11. Handle Promises Correctly

In the asynchronous world of JavaScript, promises are a prevalent concept. However, they must be handled correctly to prevent unexpected behaviour. JavaScript provides a try...catch block that can be used to handle exceptions that may arise during the execution of your asynchronous code.

This way, you ensure errors are caught and dealt with appropriately, maintaining the robustness of your code.

// ❌ Not handling promises correctly:
async function fetchData () {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  const data = await response.json()

  return data
}

// ✅ Handling promises correctly:
async function fetchData () {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
    const data = await response.json()

    return data
  } catch (error) {
    // Handle your errors here...
    throw new Error(error)
  }
}
Enter fullscreen mode Exit fullscreen mode

12. Avoid the Over-Nesting in your code

This one is one of the most common mistakes that beginners do, they nest block after block after block, a for loop inside another loop inside an if.else inside a try catch on and on.

Once you know, you have quite a clutter and you don’t know what exactly the code does and where to find the code responsible for what you’re looking for.

Debugging this type of code could be quite difficult and overwhelming, and when other programmers see it, you will confuse them too, not to mention you’ll give off the “Unprofessional” vibe.

// ❌ Nesting code blocks too much and not using the return keyword
function checkNumber (num) {
  if (num > 0) {
    console.log('Number is positive.')
  } else {
    if (num < 0) {
      console.log('Number is negative.')
    } else {
      console.log('Number is zero.')
    }
  }
}

// ✅ Using the return keyword instead of the else statement
function checkNumber (num) {
  if (num > 0) {
    console.log('Number is positive.')
    return
  }

  if (num < 0) {
    console.log('Number is negative.')
    return
  }

  console.log('Number is zero.')
}
Enter fullscreen mode Exit fullscreen mode

13. Optimizing for loops

There is a common way of writing for loops in JavaScript, here is an example of how most developers write loops in JavaScript

var languages = ['Python', 'JavaScript', 'C++', 'Ruby'];
for(var i=0;i<languages.length;i++){
  codeIn(languages[i]);
}
Enter fullscreen mode Exit fullscreen mode

For a small array like this, this loop is highly efficient and runs without any problem, but when it comes to larger datasets and an array with thousands of indexes, this approach can slow down your loop process.

What happens is that when you run languages.length the length of the array will be re-computed each time the loop runs, and the longer your array is, the more inefficient it will be to re-calculate the array length every time the loop runs especially when the length of the array is static which is in most cases.

What you should do instead is store the array length in a different variable and use that variable in your loops, for example:

var languages = ['Python', 'JavaScript', 'C++', 'Ruby'];
var langCount = languages.length;
for(var i=0;i<langCount;i++){
  codeIn(languages[i]);
}
Enter fullscreen mode Exit fullscreen mode

With this tweak, the 'length' property is accessed only once, and we use the stored value for subsequent iterations. This improves performance, particularly when dealing with large data sets.

Another elegant approach is to declare a second variable in the pre-loop statement:

var languages = ['Python', 'JavaScript', 'C++', 'Ruby'];
for(var i=0, n=languages.length; i<n; i++){
  codeIn(languages[i]);
}
Enter fullscreen mode Exit fullscreen mode

In this approach, the second variable 'n' captures the array's length in the pre-loop statement, thus achieving the same result more succinctly.

Conclusion

Remember, these practices aren't just about writing cleaner and better-structured code. They're also about producing code that's easier to maintain and debug.

Software development is a continuous process. Creating the development code is just the first step of the software life cycle. It's crucial to continually monitor, debug, and improve your code in the production stage.

So there you have it, 13 JavaScript best practices to elevate your coding game. Whether you're a beginner or an experienced developer, adopting these habits will help you write better, more efficient, and maintainable code. Happy coding!

Top comments (11)

Collapse
 
lexlohr profile image
Alex Lohr • Edited
  • #1: adopt a formatter/config and stick with it. It'll save you so much time.
  • #3: your shorter code might fall if y is falsy, like an empty string or zero. Use ?? instead of || unless this effect is intended.
  • #4: Avoid comments unless they serve a purpose, ie explaining the reasoning behind a piece of code where it's not obvious or adding helpful tooltips in lsp-powered editors/IDEs with JSDoc + markdown.
  • #5: in some cases, you need to influence CSS further than just setting a class, lest you needed classes for a thousand colors. CSS variables are a good solution.
  • #7: for..of is actually less performant than for..in and even less than for. The main use of for..of is to handle Iterators. But if performance doesn't matter too much, why not use the array methods like forEach/map/reduce? Know your loops and use the correct one. #9 seems to be missing.
  • #11: While immutability is a nice pattern, it can also be a performance problem, wasting CPU cycles and memory on unnecessary object creation and garbage collection. That being said, if you mutate arguments, make it obvious to the user.
  • #12: I really like the pattern of mixing await and .catch. It's much terser than try/catch and allows a more fine-grained error Handling.
  • #14: you really shouldn't use var anymore when there are const and let.
Collapse
 
johnlandgrave profile image
John Landgrave

In #3 the two implementations are equivalent, since the more verbose version just has if (y). I would argue that you should do a more explicit if (y === 0) or if (typeof y === 'undefined') or whatever is appropriate for your code. Relying on "falsiness" introduces a whole class of bugs if you aren't careful with your code.

Collapse
 
reacthunter0324 profile image
React Hunter

It's great practice examples. I can remember many things by this.
Thank you

Collapse
 
dcodes profile image
dcodes

Glad you liked it!

Collapse
 
szeredaiakos profile image
szeredaiakos
  1. Coding style.
    One of the most important part for me is the reduction of cognitive load on my team. It is of the utmost importance that my teammates use their own styles and language dialects. Adoption of a consistent style at a surface sounds great but it piles on busy work for both juniors and some masters. I say, if you can't read other dialects, rather than coercing others to conform, you should focus on becoming a better programmer. Automated
    formatting tools, for
    example are espe-
    cially dangerous in
    this
    regard. The-
    y make the code down
    right unread
    able sometimes.
    A very light formatting standard, however, is always welcome to reduce load on version control.

  2. Naming.
    This is an important step of the refactoring phase of a grain of work. It is not a practice, it's more like procedure.

  3. Shorthands and acronyms.
    If your grandma knows what it is, you can use it.

  4. Comments
    Comments are the first step towards refactoring. Further, if you need to document your code you are already doomed. Well, a 100k line project might benefit from some documentation.

  5. SoC
    In our company we don't sell CSS. It is NOT a concern. The only reason why one should break away code at a functional faultline is to reduce noise.

  6. Globals
    As long as a module is clean elegant and to the point, this should not have any effect. For anything else one can build application and context level global libraries.

  7. For...of
    Looping pattern should be left to the developer. Again, cognitive load reduction. Here, if a dev uses a looping scheme which he is not familiar with, increases the chance of errors.

  8. SRP.
    The author is not the only one who does not know what the single responsibility principle is.

  9. Class hoisting
    Know your language .. I guess. I do understand why this needs to be pointed out, but why specifically class hoisting? Did author ran into it? The resulting exception is particularly self explanatory.

  10. Mutation
    If you can't handle and maintain mutation you should open up a bakery instead.

  11. Promises
    The author confuses promises and dirty data/code.

  12. Nesting
    Yes, nesting is pain if you can't fit the entire hierarchy on your screen. Turning an 8k monitor portrait is not an elegant solution. Sausage conditions work, but they are sausage. And returns in the middle of functions require your team to be comfortable with it.

  13. Do optimize for loops.
    Why the iterator? There is much more precise way to show this.

Conclusion:
Best practices are still a scam. :)

Collapse
 
khaledmv profile image
Khaled Hossain

Great! Thanks

Collapse
 
dcodes profile image
dcodes

Glad you found it useful!

Collapse
 
efpage profile image
Eckehard

7. Use for...of Instead of for Loops

referring to CanIUse, for...of was not supported by some browers for a long time. First release of Safari, that supported for...of was October 2022. So, without babel it might break your code on older browsers. for...in is generally safe to use or - as mentioned below - use forEach/map/reduce instead

Collapse
 
hilleer profile image
Daniel Hillmann

Thanks for this article. Certainly some good points and considerations along!

Under point 11. Handle Promises Correctly I wonder why you recommend to try-catch always. IMO there's no reason to try-catch any promise, unless you plan to specifically handle the error thrown - if you do not, then there's no reason to try-catch it and simply throw the same error again. Furthermore, what's the reason you recommend to re-throw the error like this, after one has handled the error acconrdigly:

  } catch (error) {
    // Handle your errors here...
    throw new Error(error)
  }
Enter fullscreen mode Exit fullscreen mode

You can simply throw the already defined error - in fact, the error object do not take an error as argument but a string:

  } catch (error) {
    // Handle your errors here...
    throw error;
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ant_f_dev profile image
Anthony Fung

Great list!

There's one important thing to keep in mind about mutating function arguments. If a function takes an array and it contains references to objects, returning a new array with references to the same objects (i.e. creating a shallow copy) could cause confusion. This is because a modification to an object in one array will also affect the contents of the other array: they both refer to the same object(s).

If this isn't the intention, one way to do a deep copy is to convert an array/object into JSON, and the parsing it again.

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍