JavaScript Best Practices for Writing Clean Code
- Beta Priyoko
- Java script
- September 17, 2024
In the fast-paced world of web development, writing clean and maintainable code is more important than ever. Whether you’re working on a personal project, a client application, or contributing to a large-scale team project, clean code ensures that your work is easy to understand, scalable, and adaptable to future changes.
JavaScript, as one of the most popular and versatile programming languages, is at the heart of modern web applications. However, its flexibility can sometimes lead to messy, hard-to-read code if not managed properly. That’s why following JavaScript best practices is essential for creating high-quality applications, whether you’re building with React.js, Next.js, Express.js, or plain JavaScript.
This guide will walk you through the core principles of writing clean, effective, and maintainable JavaScript code. By incorporating these best practices into your daily development routine, you’ll not only improve the readability and performance of your applications but also make life easier for you and your teammates.
1. Use const
and let
Instead of var
One of the simplest ways to improve your JavaScript code is to stop using var
. Instead, use const
and let
which provide block scope and prevent issues related to hoisting and reassignment.
Example:
// Bad (Using var)
var name = "John";
var age = 30;
// Good (Using const and let)
const name = "John"; // Constant value that won’t change
let age = 30; // Variable value that may change later
Why?
const
prevents reassignment, making your code more predictable.let
allows block scoping, reducing the chance of accidental redeclaration.var
can lead to confusing bugs due to hoisting and function scoping.
2. Write Descriptive Variable and Function Names
Avoid using single-letter or ambiguous names for variables and functions. Use names that describe the purpose or the value that they hold.
Example:
// Bad
const n = 5;
function a() {
return n * n;
}
// Good
const numberOfItems = 5;
function calculateSquare(number) {
return number * number;
}
Why?
Descriptive names make your code more readable and maintainable. Other developers (or future you) will immediately understand what the variable or function does.
3. Keep Functions Small and Focused
Functions should perform a single task and be as short as possible. If a function is doing too many things, break it down into smaller functions.
Example:
// Bad (Too much responsibility)
function processUser(user) {
validateUser(user);
saveUserToDatabase(user);
sendWelcomeEmail(user);
}
// Good (Separation of concerns)
function processUser(user) {
validateUser(user);
saveUser(user);
sendWelcomeEmail(user);
}
function validateUser(user) {
// validation logic
}
function saveUser(user) {
// save user to database
}
function sendWelcomeEmail(user) {
// email logic
}
Why?
Small, focused functions are easier to test, debug, and reuse. This approach follows the Single Responsibility Principle (SRP).
4. Avoid Repetitive Code (DRY Principle)
The Don’t Repeat Yourself (DRY) principle is fundamental to writing clean code. Avoid duplicating logic by abstracting it into functions or reusable components.
Example:
// Bad (Repetitive code)
const taxRate = 0.2;
const priceWithTax = price * (1 + taxRate);
const salaryWithTax = salary * (1 + taxRate);
// Good (DRY)
const taxRate = 0.2;
function applyTax(amount) {
return amount * (1 + taxRate);
}
const priceWithTax = applyTax(price);
const salaryWithTax = applyTax(salary);
Why?
Avoiding repetition makes your code more maintainable and less prone to bugs. Changes only need to be made in one place.
5. Use Arrow Functions Where Appropriate
Arrow functions provide a shorter syntax and do not bind their own this
context, which makes them especially useful in callbacks.
Example:
// Bad (Function expression)
function add(a, b) {
return a + b;
}
// Good (Arrow function)
const add = (a, b) => a + b;
Why?
Arrow functions are more concise and allow for cleaner handling of the this
keyword in certain contexts, especially in React components or event handlers.
6. Use Template Literals for String Concatenation
Instead of using string concatenation with +
, use template literals for clearer, more readable string interpolation.
Example:
// Bad
const message = 'Hello, ' + name + '! You have ' + notifications + ' notifications.';
// Good
const message = `Hello, ${name}! You have ${notifications} notifications.`;
Why?
Template literals make string handling easier to read and maintain, especially when dealing with multiple variables or expressions inside strings.
7. Handle Errors Gracefully with try
/catch
When working with asynchronous code or potential failures (like network requests), ensure you handle errors properly using try/catch
blocks or .catch()
for Promises.
Example:
// Bad (No error handling)
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
// Good (With error handling)
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
return null;
}
}
Why?
Error handling prevents your application from crashing and provides better user experience by handling failures gracefully.
8. Use Default Function Parameters
When writing functions, set default values for parameters to make your code more robust and prevent potential issues with undefined
values.
Example:
// Bad
function greet(name) {
return `Hello, ${name || 'Guest'}!`;
}
// Good
function greet(name = 'Guest') {
return `Hello, ${name}!`;
}
Why?
Default parameters simplify code by providing fallbacks and reducing the need for additional checks.
9. Use Promises and async
/await
for Asynchronous Code
When dealing with asynchronous operations, use Promises and the async
/await
syntax for cleaner and more readable code.
Example:
// Bad (Using callbacks)
function fetchData(callback) {
setTimeout(() => {
callback(null, { data: 'Sample Data' });
}, 1000);
}
// Good (Using Promises with async/await)
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: 'Sample Data' });
}, 1000);
});
}
Why?
Promises and async
/await
make asynchronous code easier to read and maintain, eliminating the “callback hell.”
10. Avoid Mutating Data (Immutability)
When working with objects or arrays, avoid mutating the original data. Instead, create copies of data to prevent side effects, especially in functional programming and React development.
Example:
// Bad (Mutating an array)
const numbers = [1, 2, 3];
numbers.push(4);
// Good (Using immutable methods)
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4];
Why?
Immutability makes your code more predictable, easier to debug, and safer in a concurrent environment.
Here are a few more JavaScript best practices to expand on the original list:
11. Avoid Deep Nesting
Deeply nested code can become difficult to follow and maintain. Try to reduce the levels of nesting by refactoring your code, using early returns, or separating logic into smaller functions.
Example:
// Bad (Deep nesting)
function processUser(user) {
if (user) {
if (user.isActive) {
if (user.hasPaid) {
return 'User is active and has paid';
}
}
}
return 'Invalid user';
}
// Good (Using early returns)
function processUser(user) {
if (!user || !user.isActive || !user.hasPaid) {
return 'Invalid user';
}
return 'User is active and has paid';
}
Why?
Reducing nesting makes code more readable, easier to follow, and reduces cognitive load.
12. Use Object Destructuring
for Cleaner Code
Object destructuring allows you to extract properties from objects and arrays in a more concise and readable way.
Example:
// Bad
const user = { name: 'John', age: 30 };
const name = user.name;
const age = user.age;
// Good
const { name, age } = user;
Why?
Destructuring makes your code more concise and removes repetitive access to object properties.
13. Use Array
Methods Instead of Loops
Instead of using traditional for
loops, prefer higher-order functions like map()
, filter()
, reduce()
, and forEach()
for working with arrays. These methods are more declarative and expressive.
Example:
// Bad (Using for loop)
const numbers = [1, 2, 3, 4];
const doubleNumbers = [];
for (let i = 0; i < numbers.length; i++) {
doubleNumbers.push(numbers[i] * 2);
}
// Good (Using map)
const numbers = [1, 2, 3, 4];
const doubleNumbers = numbers.map(number => number * 2);
Why?
Array methods like map
and filter
result in cleaner, more readable code. They also emphasize immutability, since they return new arrays instead of modifying the original one.
14. Use Strict Equality (===)
Always use ===
(strict equality) instead of ==
(loose equality) to avoid unexpected type coercion.
Example:
// Bad
if (1 == '1') {
console.log('Equal'); // true, but type coercion happens
}
// Good
if (1 === '1') {
console.log('Equal'); // false, since types are not the same
}
Why?
Strict equality avoids implicit type conversion, reducing bugs caused by comparing different data types.
15. Comment Your Code When Necessary
While clean code is often self-explanatory, sometimes comments are necessary to explain the why behind complex logic or certain decisions.
Example:
// Bad (No comments for complex logic)
const calculateDiscount = (price) => price > 100 ? price * 0.9 : price;
// Good (Commenting the logic)
const calculateDiscount = (price) => {
// If the price is above 100, apply a 10% discount
return price > 100 ? price * 0.9 : price;
};
Why?
Comments can explain the reasoning behind decisions or clarify complex logic. However, avoid over-commenting or explaining obvious code.
16. Use Constants for Magic Numbers
Avoid hardcoding numbers (magic numbers) directly in your code. Instead, store them in constants with descriptive names.
Example:
// Bad (Magic number)
function calculateDiscount(price) {
return price * 0.07; // What is 0.07?
}
// Good (Using a named constant)
const TAX_RATE = 0.07;
function calculateDiscount(price) {
return price * TAX_RATE;
}
Why?
Using named constants makes your code more readable and easier to maintain, especially when numbers have specific meanings or are used in multiple places.
17. Use null
or undefined
Appropriately
Be mindful of when to use null
and undefined
. Use null
to explicitly define an “empty” value, while undefined
indicates the absence of a value (e.g., uninitialized variables).
Example:
// Bad
let name = undefined; // It's better to use null for explicitly empty values
// Good
let name = null; // Indicates an intentional empty value
Why?
Understanding the difference between null
and undefined
helps avoid unintended bugs and makes code more semantically meaningful.
18. Refactor Long switch
Statements
Avoid using long switch
statements when there are cleaner alternatives like objects or dictionaries for lookup.
Example:
// Bad (Using switch)
function getRole(role) {
switch (role) {
case 'admin':
return 'Administrator';
case 'user':
return 'Regular User';
case 'guest':
return 'Guest';
default:
return 'Unknown Role';
}
}
// Good (Using object lookup)
const roles = {
admin: 'Administrator',
user: 'Regular User',
guest: 'Guest',
};
function getRole(role) {
return roles[role] || 'Unknown Role';
}
Why?
Object lookups are more readable and easier to maintain than long switch
statements.
19. Avoid Side Effects in Functions
A function should, whenever possible, avoid mutating variables outside of its scope (side effects). Pure functions that only rely on their inputs are easier to test and debug.
Example:
// Bad (Side effect)
let counter = 0;
function increment() {
counter++;
}
// Good (Pure function)
function increment(counter) {
return counter + 1;
}
Why?
Pure functions are predictable and less prone to introducing bugs, making them a cornerstone of functional programming.
20. Use ESLint and Prettier
To ensure consistent code formatting and catch potential issues early, use tools like ESLint and Prettier. These tools can be integrated into your workflow to enforce clean code standards automatically.
Example:
- ESLint: Helps identify and fix common code issues and enforces coding standards.
- Prettier: Automatically formats code for consistency and readability.
Why?
These tools help maintain code quality, consistency, and prevent potential bugs early in the development process.
Here are a few more advanced JavaScript best practices that can further improve your clean coding approach:
21. Use async
/await
Over then()
for Promises
Using async
/await
improves readability and simplifies chaining of promises. It also helps avoid callback hell.
Example:
// Bad (Using .then)
fetchUserData()
.then((user) => fetchUserPosts(user.id))
.then((posts) => displayPosts(posts))
.catch((error) => handleError(error));
// Good (Using async/await)
async function fetchAndDisplayUserData() {
try {
const user = await fetchUserData();
const posts = await fetchUserPosts(user.id);
displayPosts(posts);
} catch (error) {
handleError(error);
}
}
Why?
async
/await
is more readable and makes error handling with try
/catch
clearer compared to using .then()
and .catch()
for chaining promises.
22. Avoid Modifying Function Parameters
Changing function parameters within a function can introduce bugs, especially when passing objects or arrays by reference. Always treat function parameters as immutable and return new values instead of modifying the original ones.
Example:
// Bad (Modifying parameter)
function updateUser(user) {
user.isAdmin = true;
return user;
}
// Good (Creating a new object)
function updateUser(user) {
return { ...user, isAdmin: true };
}
Why?
Keeping function parameters immutable prevents unexpected side effects, especially when working with shared data in a complex application.
23. Avoid Using the new
Keyword for Object Creation
Instead of using the new
keyword to create objects, consider using factory functions or classes to handle object instantiation. This approach is cleaner, avoids issues with this
context, and is more predictable.
Example:
// Bad (Using new with function constructor)
function Person(name) {
this.name = name;
}
const person = new Person('John');
// Good (Using class or factory function)
class Person {
constructor(name) {
this.name = name;
}
}
const person = new Person('John');
Why?
Using classes or factory functions makes your code more consistent and easier to work with, especially for object creation and inheritance.
24. Use Functional Programming Principles
Incorporate functional programming principles like pure functions, higher-order functions, and immutability. Avoid mutating state directly and use methods like map
, filter
, and reduce
to process data without side effects.
Example:
// Bad (Imperative, mutating array)
const numbers = [1, 2, 3, 4];
for (let i = 0; i < numbers.length; i++) {
numbers[i] *= 2;
}
// Good (Declarative, using map)
const numbers = [1, 2, 3, 4];
const doubled = numbers.map((n) => n * 2);
Why?
Functional programming leads to more predictable, testable, and maintainable code by emphasizing immutability and reducing side effects.
25. Use Optional Chaining and Nullish Coalescing
Optional chaining (?.
) allows you to safely access deeply nested properties, avoiding runtime errors when a property doesn’t exist. Nullish coalescing (??
) provides a default value only when null
or undefined
is encountered.
Example:
// Bad
const user = {};
const city = user && user.address && user.address.city;
// Good (Using optional chaining and nullish coalescing)
const user = {};
const city = user?.address?.city ?? 'Unknown';
Why?
Optional chaining and nullish coalescing simplify your code and prevent runtime errors when accessing potentially null
or undefined
values.
26. Use for...of
and for...in
Instead of forEach
When Necessary
Although .forEach()
is a common way to iterate through arrays, for...of
provides greater flexibility, such as allowing the use of break
or continue
statements.
Example:
// Bad (Using forEach without control flow)
[1, 2, 3, 4].forEach((num) => {
if (num === 3) {
return; // This doesn't break out of the loop, just skips the current iteration
}
console.log(num);
});
// Good (Using for...of with break/continue)
for (const num of [1, 2, 3, 4]) {
if (num === 3) continue; // Skips this iteration
console.log(num);
}
Why?
for...of
allows greater control in loops with features like break
and continue
, which cannot be used with .forEach()
.
27. Prefer Named Exports Over Default Exports
Using named exports makes it easier to identify and import exactly what you need from a module. It also helps avoid issues with default exports when refactoring code.
Example:
// Bad (Using default export)
export default function calculateTotal() {
//...
}
// Good (Using named export)
export function calculateTotal() {
//...
}
Why?
Named exports provide better auto-completion in editors and prevent name collisions, making it clearer which module you are importing.
28. Avoid Using eval()
Using eval()
can be dangerous as it executes arbitrary code, making your code vulnerable to attacks like code injection. Avoid it in all circumstances.
Example:
// Bad (Using eval)
const code = 'console.log("Hello World")';
eval(code); // This is dangerous and prone to security issues
// Good (Avoid eval)
const code = 'console.log("Hello World")';
// Manually execute or refactor the code without eval
Why?
eval()
can introduce security vulnerabilities and lead to unpredictable behavior. Modern JavaScript rarely, if ever, requires eval()
to solve a problem.
29. Use .bind()
or Arrow Functions to Preserve this
in Callbacks
In event handlers or callbacks, this
can refer to different contexts. Use .bind()
or arrow functions to explicitly bind this
to the appropriate context.
Example:
// Bad (Incorrect use of this in callback)
class MyComponent {
handleClick() {
console.log(this); // `this` will not refer to the class instance
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
// Good (Using arrow function or bind to preserve this)
class MyComponent {
handleClick = () => {
console.log(this); // `this` refers to the class instance
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
Why?
Using .bind()
or arrow functions ensures that this
refers to the correct context, avoiding potential bugs in event handlers or callbacks.
30. Use Immutable Data Structures When Possible
In certain contexts (e.g., React or Redux), using immutable data structures like Immutable.js or native JavaScript methods that don’t mutate data (map
, filter
, reduce
) helps prevent unintended side effects and ensures data consistency.
Example:
// Bad (Mutating original array)
const numbers = [1, 2, 3];
numbers.push(4);
// Good (Using immutable approach)
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4];
Why?
Immutability ensures data is not inadvertently modified in ways that affect other parts of the application, leading to more predictable and reliable code.
Conclusion
By applying these advanced best practices in your JavaScript code, you’ll be able to write clean, efficient, and maintainable applications, whether in React.js, Node.js, or any other environment. These techniques help streamline the development process, improve code readability, and reduce the chances of bugs and issues down the line.