Writing code that works is just the beginning. Writing code that's clean, maintainable, and professional separates good developers from great ones. Best practices aren't arbitrary rules—they're lessons learned from decades of software development. From naming conventions to error handling, from code organization to performance optimization, these principles help you write code that you and your team will be proud of. Let's master JavaScript best practices!
Clean Code Principles
Clean Code Characteristics:
- Readable - easy to understand at a glance
- Maintainable - easy to modify and extend
- Simple - does one thing well
- Testable - easy to verify correctness
- Self-documenting - code explains itself
- Consistent - follows predictable patterns
Naming Conventions
Descriptive names improve readability
Bad Naming
// Bad: Unclear names
let d = new Date();
let x = 10;
let arr = [];
function calc(a, b) {
return a * b * 0.2;
}
// Cryptic abbreviations
let usrNm = 'Alice';
let tmpVal = getData();
// Inconsistent naming
let UserName = 'Bob';
let user_age = 25;
let USERTYPE = 'admin';Good Naming
// Good: Descriptive names
let currentDate = new Date();
let maxRetries = 10;
let users = [];
function calculateDiscount(price, quantity) {
const DISCOUNT_RATE = 0.2;
return price * quantity * DISCOUNT_RATE;
}
// Clear and full words
let username = 'Alice';
let temporaryValue = getData();
// Consistent camelCase
let userName = 'Bob';
let userAge = 25;
let userType = 'admin';// Naming Conventions
// Variables and functions: camelCase
let firstName = 'Alice';
let totalAmount = 100;
function calculateTotal() { }
function getUserData() { }
// Constants: UPPER_SNAKE_CASE
const MAX_USERS = 100;
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_TIMEOUT = 5000;
// Classes: PascalCase
class User { }
class ShoppingCart { }
class ProductManager { }
// Private properties: prefix with underscore (convention)
class BankAccount {
constructor() {
this._balance = 0; // Private
this.accountNumber = '123'; // Public
}
}
// Booleans: use is/has/should prefix
let isValid = true;
let hasPermission = false;
let shouldRetry = true;
// Arrays: plural nouns
let users = [];
let products = [];
let orders = [];
// Functions: verb + noun
function getUser() { }
function createOrder() { }
function validateEmail() { }
function calculateTotal() { }
// Event handlers: handle + Event
function handleClick() { }
function handleSubmit() { }
function handleKeyPress() { }Variable Declarations
Use const/let, declare explicitly
Bad Variable Practices
// Bad: Using var
var count = 0;
var message = 'Hello';
// Bad: Missing declarations
function example() {
result = 10; // Implicit global!
}
// Bad: Declaring at bottom
function process() {
doSomething();
calculate();
let x = 5; // Declared late
}
// Bad: Multiple vars
var a = 1, b = 2, c = 3;Good Variable Practices
// Good: Use const by default
const MAX_ITEMS = 10;
const API_URL = 'https://api.com';
// Good: Use let for reassignment
let count = 0;
count++;
// Good: Always declare
function example() {
let result = 10; // Properly declared
}
// Good: Declare at top
function process() {
let x = 5; // Declared first
doSomething();
calculate();
}
// Good: One per line
const a = 1;
const b = 2;
const c = 3;Function Best Practices
Single responsibility, manageable parameters
Bad Function Design
// Bad: Long, multi-purpose function
function processUser(user) {
// Validate
if (!user.email || !user.email.includes('@')) {
return false;
}
// Transform
user.email = user.email.toLowerCase();
user.name = user.name.trim();
// Save to database
db.save(user);
// Send email
sendEmail(user.email, 'Welcome!');
// Log
console.log('User processed:', user);
// Update UI
updateUI(user);
return true;
}
// Too many parameters
function createUser(name, email, age, city,
country, phone, address, zip) {
// ...
}Good Function Design
// Good: Single responsibility
function validateUser(user) {
if (!user.email || !user.email.includes('@')) {
throw new Error('Invalid email');
}
}
function normalizeUser(user) {
return {
...user,
email: user.email.toLowerCase(),
name: user.name.trim()
};
}
function saveUser(user) {
return db.save(user);
}
// Good: Options object for many params
function createUser(userData) {
const {
name,
email,
age,
city,
country,
phone,
address,
zip
} = userData;
// ...
}// Function Best Practices
// 1. Do one thing well
// Bad
function updateUserAndSendEmail(user, message) {
user.lastLogin = new Date();
updateDatabase(user);
sendEmail(user.email, message);
}
// Good
function updateUserLastLogin(user) {
user.lastLogin = new Date();
return updateDatabase(user);
}
function sendWelcomeEmail(user) {
return sendEmail(user.email, 'Welcome!');
}
// 2. Return early
// Bad
function getDiscount(user) {
let discount = 0;
if (user) {
if (user.isPremium) {
if (user.orders > 10) {
discount = 0.2;
} else {
discount = 0.1;
}
}
}
return discount;
}
// Good
function getDiscount(user) {
if (!user) return 0;
if (!user.isPremium) return 0;
if (user.orders > 10) return 0.2;
return 0.1;
}
// 3. Avoid side effects
// Bad - modifies input
function addTax(product) {
product.price = product.price * 1.1;
return product;
}
// Good - returns new object
function addTax(product) {
return {
...product,
price: product.price * 1.1
};
}
// 4. Use default parameters
// Bad
function greet(name) {
name = name || 'Guest';
return `Hello, ${name}`;
}
// Good
function greet(name = 'Guest') {
return `Hello, ${name}`;
}
// 5. Descriptive function names
// Bad
function proc(d) { }
function doIt() { }
// Good
function processPayment(order) { }
function validateUserInput() { }Error Handling
Handle errors properly with context
Bad Error Handling
// Bad: Silent failures
function getUser(id) {
try {
return db.find(id);
} catch (error) {
// Swallowing error - bad!
}
}
// Bad: Generic error messages
if (!user) {
throw new Error('Error');
}
// Bad: Not handling errors
async function loadData() {
let data = await fetch('/api/data');
return data.json();
// No error handling!
}Good Error Handling
// Good: Proper error handling
function getUser(id) {
try {
return db.find(id);
} catch (error) {
console.error('Failed to get user:', error);
throw error; // Re-throw
}
}
// Good: Descriptive errors
if (!user) {
throw new Error(`User not found: ID ${userId}`);
}
// Good: Try-catch with meaningful handling
async function loadData() {
try {
let response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to load data:', error);
return null; // Graceful fallback
}
}// Error Handling Best Practices
// 1. Use try-catch for async operations
async function fetchUserData(userId) {
try {
let response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching user:', error);
throw error; // Let caller handle
}
}
// 2. Create custom error types
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
function validateEmail(email) {
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
}
// 3. Fail fast
function processOrder(order) {
if (!order) {
throw new Error('Order is required');
}
if (!order.items || order.items.length === 0) {
throw new Error('Order must have items');
}
if (order.total <= 0) {
throw new Error('Order total must be positive');
}
// Process order...
}
// 4. Provide context in errors
function divideNumbers(a, b) {
if (b === 0) {
throw new Error(`Cannot divide ${a} by zero`);
}
return a / b;
}
// 5. Handle specific error types
try {
validateEmail(email);
processPayment(order);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error.message.includes('payment')) {
showPaymentError(error.message);
} else {
showGenericError();
}
}Code Organization
// File Structure Example
// constants.js - Configuration and constants
export const API_BASE_URL = 'https://api.example.com';
export const MAX_RETRIES = 3;
export const TIMEOUT = 5000;
export const USER_ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
};
// utils.js - Utility functions
export function formatDate(date) {
return date.toLocaleDateString();
}
export function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// api.js - API calls
import { API_BASE_URL } from './constants.js';
export async function fetchUsers() {
let response = await fetch(`${API_BASE_URL}/users`);
return response.json();
}
export async function createUser(userData) {
let response = await fetch(`${API_BASE_URL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}
// User.js - User class
export class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
get displayName() {
return this.name.split(' ')[0];
}
}
// main.js - Main application logic
import { fetchUsers, createUser } from './api.js';
import { User } from './User.js';
import { formatDate } from './utils.js';
async function initializeApp() {
let users = await fetchUsers();
displayUsers(users);
}
initializeApp();Comments and Documentation
Comment WHY, not WHAT
Bad Comments
// Bad: Obvious comments
// Increment i
i++;
// Loop through array
for (let i = 0; i < arr.length; i++) {
// Do something
doSomething(arr[i]);
}
// Bad: Commented-out code
// function oldFunction() {
// // old implementation
// }
// Bad: Misleading comments
// Returns user object
function getUsers() { // Actually returns array!
return [...];
}Good Comments
// Good: Explain WHY, not WHAT
// Cache results to avoid repeated API calls
let cachedUsers = null;
// Good: Document complex logic
// Using binary search for O(log n) performance
// on sorted array of 10k+ items
function findUser(id) {
// ...
}
// Good: JSDoc for functions
/**
* Calculates discounted price
* @param {number} price - Original price
* @param {number} discount - Discount (0-1)
* @returns {number} Final price
*/
function applyDiscount(price, discount) {
return price * (1 - discount);
}// Documentation Best Practices
// 1. JSDoc for public APIs
/**
* Fetches user data from the API
* @param {string} userId - The user's unique identifier
* @param {Object} options - Optional configuration
* @param {boolean} options.includeOrders - Include order history
* @param {number} options.timeout - Request timeout in ms
* @returns {Promise<User>} User object with profile data
* @throws {Error} If user not found or network error
* @example
* const user = await fetchUser('123', { includeOrders: true });
*/
async function fetchUser(userId, options = {}) {
// Implementation...
}
// 2. Explain complex algorithms
function calculateShippingCost(weight, distance) {
// Using tiered pricing model:
// 0-10 lbs: $5 base + $0.50/mile
// 10-50 lbs: $10 base + $0.75/mile
// 50+ lbs: $20 base + $1.00/mile
let baseRate, perMile;
if (weight <= 10) {
baseRate = 5;
perMile = 0.50;
} else if (weight <= 50) {
baseRate = 10;
perMile = 0.75;
} else {
baseRate = 20;
perMile = 1.00;
}
return baseRate + (distance * perMile);
}
// 3. Document non-obvious decisions
function processPayment(order) {
// Using exponential backoff for retries
// because payment gateway is rate-limited
// to 100 requests per minute
const maxRetries = 3;
let delay = 1000;
for (let i = 0; i < maxRetries; i++) {
try {
return await paymentGateway.charge(order);
} catch (error) {
await sleep(delay);
delay *= 2; // Exponential backoff
}
}
}
// 4. TODO comments with context
// TODO: Implement caching after user feedback
// Current approach makes 3 API calls per page load
// Caching could reduce to 1 call, but need to handle
// cache invalidation for real-time updates
function loadDashboard() {
// ...
}Performance Best Practices
// Performance Best Practices
// 1. Avoid unnecessary DOM queries
// Bad
for (let i = 0; i < items.length; i++) {
document.getElementById('list').innerHTML += `<li>${items[i]}</li>`;
}
// Good - query once, build string
let list = document.getElementById('list');
let html = items.map(item => `<li>${item}</li>`).join('');
list.innerHTML = html;
// 2. Debounce expensive operations
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Use for search input
let searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
performSearch(e.target.value);
}, 300));
// 3. Use event delegation
// Bad - many event listeners
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick);
});
// Good - single listener on parent
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.classList.contains('item')) {
handleClick(e);
}
});
// 4. Cache computed values
// Bad - recalculates every time
function getExpensiveValue() {
return items.reduce((sum, item) => {
return sum + complexCalculation(item);
}, 0);
}
// Good - cache result
let cachedValue = null;
function getExpensiveValue() {
if (cachedValue === null) {
cachedValue = items.reduce((sum, item) => {
return sum + complexCalculation(item);
}, 0);
}
return cachedValue;
}
// 5. Use appropriate data structures
// Bad - array for lookups
let users = [/* thousands of users */];
let user = users.find(u => u.id === targetId); // O(n)
// Good - Map for fast lookups
let userMap = new Map(users.map(u => [u.id, u]));
let user = userMap.get(targetId); // O(1)
// 6. Avoid memory leaks
// Bad - event listener not removed
function setupWidget() {
let element = document.getElementById('widget');
element.addEventListener('click', handleClick);
// If widget removed, listener still exists!
}
// Good - clean up
function setupWidget() {
let element = document.getElementById('widget');
let handler = () => handleClick();
element.addEventListener('click', handler);
return function cleanup() {
element.removeEventListener('click', handler);
};
}Security Best Practices
// Security Best Practices
// 1. Never trust user input
function createUserProfile(userData) {
// Bad - directly using user input
document.getElementById('name').innerHTML = userData.name; // XSS risk!
// Good - sanitize or use textContent
document.getElementById('name').textContent = userData.name;
}
// 2. Validate on both client and server
function validateEmail(email) {
// Client-side validation is for UX, not security
let pattern = /^[\w.-]+@[\w.-]+\.[a-z]{2,}$/i;
return pattern.test(email);
}
// Always validate on server too!
// app.post('/register', (req, res) => {
// if (!validateEmail(req.body.email)) {
// return res.status(400).send('Invalid email');
// }
// });
// 3. Don't expose sensitive data
// Bad - API key in client code
const API_KEY = 'sk_live_12345'; // Exposed to everyone!
fetch(`https://api.com/data?key=${API_KEY}`);
// Good - API key on server
// Server-side code handles API key
// Client just calls your backend
fetch('/api/data'); // Your server adds the key
// 4. Use HTTPS for sensitive data
// Bad - sending password over HTTP
fetch('http://example.com/login', {
method: 'POST',
body: JSON.stringify({ password: 'secret' })
});
// Good - use HTTPS
fetch('https://example.com/login', {
method: 'POST',
body: JSON.stringify({ password: 'secret' })
});
// 5. Implement proper authentication
// Bad - storing password in localStorage
localStorage.setItem('password', userPassword); // Never do this!
// Good - store only tokens, with expiration
function storeAuthToken(token, expiresIn) {
let expiry = Date.now() + (expiresIn * 1000);
sessionStorage.setItem('authToken', JSON.stringify({
token,
expiry
}));
}
// 6. Prevent XSS
// Bad - eval with user input
eval(userInput); // Extremely dangerous!
// Bad - innerHTML with user data
element.innerHTML = userComment; // XSS risk
// Good - textContent or createElement
element.textContent = userComment;
// Or sanitize if HTML needed
function sanitizeHTML(html) {
let temp = document.createElement('div');
temp.textContent = html;
return temp.innerHTML;
}Code Quality Checklist
Before Committing Code:
- ✓ Descriptive variable and function names
- ✓ Functions are small and focused
- ✓ No magic numbers (use named constants)
- ✓ Proper error handling
- ✓ Input validation
- ✓ No console.logs in production
- ✓ Consistent code formatting
- ✓ Comments explain WHY, not WHAT
- ✓ No unused variables or imports
- ✓ Code is DRY (Don't Repeat Yourself)
- ✓ Security considerations addressed
- ✓ Performance considerations for loops/queries
Key Takeaways
- Use descriptive names—code should be self-documenting
- Use const by default, let when needed, never var
- Functions should do one thing well
- Handle errors properly with meaningful messages
- Keep code DRY—don't repeat yourself
- Comment WHY, not WHAT
- Organize code into logical modules
- Validate all user input
- Consider performance for critical operations
- Never trust client-side validation alone
- Keep functions small (under 20-30 lines ideally)
- Write code for humans first, computers second
What's Next?
You now understand JavaScript best practices and can write clean, maintainable, professional code! You've learned naming conventions, code organization, error handling, performance optimization, and security considerations.
In the next lesson, we'll explore Debugging Techniques and Tools—learning to use browser DevTools, set breakpoints, and troubleshoot issues effectively!
💪 Practice Challenge:
Before moving on, try:
- Refactor old code following best practices
- Review naming conventions in your projects
- Add proper error handling to functions
- Organize code into logical modules
- Document complex functions with JSDoc