Skip to main content

Clean Code JavaScript

· 10 min read
Bruno Carneiro
Fundador da @TautornTech

In this article, I will present some good programming practices in JavaScript. However, these practices can be applied to any programming language, as they represent a set of recommended guidelines.

The central concept addressed here is "Clean Code," which refers to code that is easy to read and understand, as well as easy to maintain. The goal is to create clear and concise code.

Before proceeding, I would like to start with a quote from Bjarne Stroustrup, the creator of the C++ language:

"I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an articulated strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean code does one thing well."

  1. Variables
  2. Functions
  3. Conditionals
  4. Objects
  5. Arrays
  6. Concurrency
  7. Error Handling

Variables

Names

  • Use names that clearly describe the purpose of the variable.
  • Avoid generic names such as a, b, c, x, y, and z.
// Bad
const start = new Date();
const dateE = new Date();
const j = 'Tautorn';

// Good
const startDate = new Date();
const endDate = new Date();
const userName = 'Tautorn';

In some cases, using longer names for variables can be a good option, as long as the name accurately describes what the variable represents.

componentDidMount is an example of a function name that describes well what it does. startLifeCycleAfterRenderer is an example of a function name that describes well what it does, but it is a very long name.

The same principle applies to function names:

// bad
const getId = () => {
// ...
}

// good
const getUserId = () => {
// ...
}

Convention

  • Use camelCase for naming variables and functions.
  • Use PascalCase for naming classes.
  • Use UPPER_CASE for naming constants.
// Bad
const user_name = 'Tautorn'

class userList {}

const Range_limitForLoan = 100

// Good
const userName = 'Tautorn'

class UserList {}

const RANGE_LIMIT_FOR_LOAN = 100

Avoid Global Variables

Global variables should be avoided, as they can be accessed and modified from anywhere in the code, which can lead to hard-to-identify problems.

Do Not Use Magic Numbers

Magic numbers are numeric values used directly in code without explanation. Avoid this by assigning these values to meaningful constants.

// Bad
const getDiscount = (value) => {
if (value > 100) {
return value * 0.1
} else {
return value * 0.05
}
}

// Good
const DISCOUNT_VALUE = 0.1
const DISCOUNT_VALUE_DEFAULT = 0.05
const DISCOUNT_LIMIT = 100

const getDiscount = (value) => {
if (value > DISCOUNT_LIMIT) {
return value * DISCOUNT_VALUE
} else {
return value * DISCOUNT_VALUE_DEFAULT
}
}

Functions:

Use arrow functions for short functions and callbacks:

// Bad
function sanitizeName(name) {
return name.trim().toLowerCase()
}

// Good
const sanitizeName = (name) => name.trim().toLowerCase()

Functions with single responsibility:

  • Functions should do only one thing. This is the single responsibility principle.

Bad:

const getUserNameAndSalary = (id) => {
const response = await fetch(`api.com/${id}`)
const userData = await response.json()

const responseSalary = await fetch(`api.com/salary/${id}`)
const userSalary = await responseSalary.json()

return {
name: userData.name,
salary: userSalary.salary
}
}

In addition to mixing two requests, maintenance becomes more difficult, along with testing and evolution.

Good:

const getUserName = () => {
const response = await fetch(`api.com/${id}`)
const userData = await response.json()

return {
name: userData.name
}
}

const getUserSalary = () => {
const responseSalary = await fetch(`api.com/salary/${id}`)
const userSalary = await responseSalary.json()

return {
salary: userSalary.salary
}
}

This way, each function has a single responsibility, making code maintenance and understanding easier.

Of course we could have a single function called getUser that would bring all user data. But I just wanted to bring an example of a method doing two things that should be isolated.

Functions should have at most 3 parameters

Bad:

const getCarContext = (id, color, weight, model, engine) => {
// ...
}

This greatly hinders the evolution of the function, increases complexity, and whoever calls the function may also have difficulty with the parameters (typing helps but I am not covering that in this article).

Good:

const getCarContext = (id) => {
// ...
}

But since we are using JavaScript it is possible to expect an object as a parameter, thus avoiding the problem of too many parameters.

const getLocation = ({ city, state, country }) => {
// ...
}

The format above, with an object, is for me the best scenario because whoever is calling the function does not need to know each parameter the function expects based on its position, e.g.:

// bad
getLocation('São Paulo', 'SP', 'Brasil')

// good
getLocation({
city: 'São Paulo',
state: 'SP',
country: 'Brasil'
})

This way it is easier to understand what the function expects and avoids silly mistakes.

We can also perform destructuring on the function parameter to make it easier to use:

const getLocation = (props) => {
const { city, state, country } = props
// ...
}

This is a good way to avoid too many parameters in a function.

Pure Functions

Pure functions are functions that do not alter the state of anything — that is, they do not change the value of a variable, an object, or an array.

// bad

let userName = 'value'
const getUserName = (name) => {
userName = name
// ...
}

// good
const getUserName = (name) => {
const userName = name
// ...
}

Avoiding changing the value of a variable or object in different places helps prevent side effects and makes errors easier to identify.


Conditionals:

Avoid Using If Else

Using many if else blocks in the middle of code can make it complex and difficult to understand. Instead, use ternary operators or data structures to simplify the code.


// Bad
const getProduct = (id) => {
const response = await fetch(`api.com/${id}`)
const product = await response.json()

if (product.value > 100) {
return {
...product,
discount: 10
}
} else {
return {
...product,
discount: 5
}
}
}

// Good
const getProduct = (id) => {
const response = await fetch(`api.com/${id}`)
const product = await response.json()

const discount = product.value > 100 ? 10 : 5

return {
...product,
discount
}
}

The Good example is much more readable and easier to maintain than the Bad example. A ternary made things simpler.

There are magic numbers in the function, but I will address that later.

Note that I also left only one return in the function — this is the best scenario. Functions with multiple return statements are quite problematic.


// Bad
const getAuthenticateSession = (token) => {
let isActive = false
const limitTime = 1000 * 60 * 60 * 24 * 30 // 30 days

if (!token) {
return false
}

const tokenDate = new Date(token.date)
const currentDate = new Date()

const isValid = currentDate - tokenDate > limitTime

if (isValid) {
const tokenDate = new Date(token.date)
isActive = true
}

if (isActive) {
return {
status: 'active',
message: 'Active session'
date: new Date()
}
} else {
return {
status: 'inactive',
message: 'Inactive session'
date: new Date()
}
}
}

// Good
const tokenIsValid = (token) => {
const limitTime = 1000 * 60 * 60 * 24 * 30 // 30 days

if (!token) {
return false
}

const tokenDate = new Date(token.date)
const currentDate = new Date()

return currentDate - tokenDate > limitTime
}

const getAuthenticateSession = (token) => {
const isActive = tokenIsValid(token)

return{
status: isActive ? 'active' : 'inactive',
message: isActive ? 'Active session' : 'Inactive session',
date: new Date()
}
}

Use Literal Objects Instead of if else and switch/case

// Bad
function getCategory(category) {
if (category === 'food') {
return 'food'
} else if (category === 'drink') {
return 'drink'
} else if (category === 'dessert') {
return 'dessert'
} else {
return 'other'
}
}

// Bad
function getCategory(category) {
switch (category) {
case 'food':
return 'food'
case 'drink':
return 'drink'
case 'dessert':
return 'dessert'
default:
return 'other'
}

// Good
function getCategory(category) {
const categories = {
food: 'food',
drink: 'drink',
dessert: 'dessert',
other: 'other'
}

return categories[category] || 'other'
}

This approach applies to many cases, even for returning callbacks, for example:

function getFinanciersCallBack(type) {
const financiers = {
bank: () => {
// ...
},
creditCard: () => {
// ...
},
loan: () => {
// ...
}
}

return financiers[type] || () => {
// ...
}
}

Objects:

Use getter and setter to access object properties

Getter and setter methods provide control over the manipulation of object properties, allowing validations before accessing or modifying values.

const person = {
_name: 'John',
get name() {
return this._name;
},
set name(newName) {
if (typeof newName === 'string') {
this._name = newName;
} else {
console.error('O nome deve ser uma string.');
}
}
};

console.log(person.name); // Gets the name
person.name = 'Alice'; // Sets the name
person.name = 42; // Validation error

Use Descriptive Properties

When defining object properties, use names that clearly describe what the property represents.

// Bad
const location = {
c: 'Brasil',
s: 'SP',
ct: 'São Paulo'
}

// Good
const location = {
country: 'Brasil',
state: 'SP',
city: 'São Paulo'
}

Cloning Objects

Use spread or Object.assign to clone objects instead of direct assignments. This way side effects are avoided when modifying the original object.

// Bad
const clonedObject = originalObject

// Good
const clonedObject = { ...originalObject }

or

const clonedObject = Object.assign({}, originalObject)


Arrays:

Use map, filter, reduce instead of for

// Bad

const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]

const usersWithAge = []

for (let i = 0; i < users.length; i++) {
usersWithAge.push({
...users[i],
age: users[i].age + 1
})
}

// Good

const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]

const usersWithAge = users.map((user) => ({
...user,
age: user.age + 1
}))

Use spread to copy arrays

// Bad
const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]
const usersCopy = []

for (let i = 0; i < users.length; i += 1) {
itemsCopy[i] = items[i];
}


// Good
const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]

const usersCopy = [...users]

Concurrency:

Callback hell

Callback hell is when we have many nested callbacks; this makes the code very difficult to read and maintain.

// Bad
getProfile(function(user)) {
getArticles(function(articles) {
getComments(function(comments) {
getLikes(function(likes) {
// ...
})
})
})
}

// Good
getProfile(user)
.then(getArticles)
.then(getComments)
.then(getLikes)
.then((likes) => {
// ...
})

Error Handling:

Do not ignore caught errors and know how to organize errors. It is very important to know what to do with errors — in many cases they are ignored and this can generate problems that are very difficult to identify. Identifying problems when the code is already in production can be a difficult task when error handling was not well thought out/executed.

// Bad
try {
functionCausingError()
} catch (error) {
console.log(error)
}

// Good
try {
functionCausingError()
} catch (error) {
// Console error is a better alert than using console.log
console.error(`Error description ${error}`)

generateLog(error)

notifyUser(error)
}

Try/catch

The same applies using try/catch; here is an example with async/await.

// Bad

const getProfile = async (id) => {
try {
const response = await fetch(`api.com/${id}`)
const profile = await response.json()

return profile
} catch (error) {
console.log(error)
}
}

// Good

const getProfile = async (id) => {
try {
const response = await fetch(`api.com/${id}`)
const profile = await response.json()

return profile
} catch (error) {
// Console error is a better alert than using console.log
console.error(`Error description ${error}`)

generateLog(error)

notifyUser(error)
}
}

The logic was the same, but now we are handling the error in a better way.


Conclusion

In this article we saw some good programming practices using JavaScript. But this can be applied to any programming language — it is a best practice.

The boy scout rule:

"Always leave the campground cleaner than you found it."

Using the same idea but bringing it to programming:

"Always leave the code cleaner than you found it."

By following the practices of descriptive naming, single-responsibility functions, effective error handling, avoiding excessive conditionals, and other guidelines presented in this article, you can create more readable, maintainable, and less error-prone code.

By adopting these best practices, you not only improve the quality of your code, but also make the development and maintenance of software a more efficient and satisfying experience. Remember that clean code is an investment in the present and future of your project.

However, there is no single universal approach to writing code. Sometimes there may be situations where the practices mentioned need to be adjusted to the specific needs of the project. These guidelines provide a solid foundation for improving the quality of your code while also making your life as a developer more pleasant.

References