JavaScript powers the web, but writing good JavaScript requires discipline. Whether you're building frontends, backends, or full-stack apps, these best practices will help you write cleaner, safer, and more maintainable code.
Variables and Declarations
Use const by Default, let When Needed
// Bad
var name = 'John';
var items = [1, 2, 3];
// Good
const name = 'John'; // Never reassigned
const items = [1, 2, 3]; // Array reference doesn't change
let count = 0; // Will be reassigned
count++;Use Descriptive Names
// Bad
const d = new Date();
const u = users.filter((x) => x.a > 18);
// Good
const currentDate = new Date();
const adultUsers = users.filter((user) => user.age > 18);Avoid Magic Numbers
// Bad
if (password.length < 8) { ... }
setTimeout(retry, 3000);
// Good
const MIN_PASSWORD_LENGTH = 8;
const RETRY_DELAY_MS = 3000;
if (password.length < MIN_PASSWORD_LENGTH) { ... }
setTimeout(retry, RETRY_DELAY_MS);Functions
Keep Functions Small and Focused
// Bad - does too many things
function processUser(userData) {
// validate
if (!userData.email) throw new Error('Email required');
if (!userData.name) throw new Error('Name required');
// normalize
userData.email = userData.email.toLowerCase().trim();
userData.name = userData.name.trim();
// save
return db.users.insert(userData);
}
// Good - separated concerns
function validateUser(data) {
if (!data.email) throw new Error('Email required');
if (!data.name) throw new Error('Name required');
}
function normalizeUser(data) {
return {
...data,
email: data.email.toLowerCase().trim(),
name: data.name.trim(),
};
}
function createUser(userData) {
validateUser(userData);
const normalized = normalizeUser(userData);
return db.users.insert(normalized);
}Use Default Parameters
// Bad
function createUser(name, role) {
role = role || 'user';
}
// Good
function createUser(name, role = 'user') {
// ...
}Use Arrow Functions for Callbacks
// Good - arrow functions for short callbacks
const names = users.map((user) => user.name);
const adults = users.filter((user) => user.age >= 18);
const total = prices.reduce((sum, price) => sum + price, 0);Return Early to Avoid Nesting
// Bad - deeply nested
function getDiscount(user) {
if (user) {
if (user.isPremium) {
if (user.yearsActive > 5) {
return 0.3;
} else {
return 0.2;
}
} else {
return 0.05;
}
} else {
return 0;
}
}
// Good - early returns
function getDiscount(user) {
if (!user) return 0;
if (!user.isPremium) return 0.05;
if (user.yearsActive > 5) return 0.3;
return 0.2;
}Objects and Arrays
Use Destructuring
// Bad
const name = user.name;
const email = user.email;
const first = items[0];
// Good
const { name, email } = user;
const [first] = items;
// Function parameters
function greet({ name, age }) {
return `Hello ${name}, you are ${age}`;
}Use Spread for Immutable Updates
// Objects
const updated = { ...user, name: 'Jane' };
// Arrays
const added = [...items, newItem];
const removed = items.filter((item) => item.id !== targetId);
const replaced = items.map((item) =>
item.id === targetId ? { ...item, done: true } : item,
);Use Optional Chaining and Nullish Coalescing
// Bad
const city = user && user.address && user.address.city;
const name = user.name !== null && user.name !== undefined ? user.name : 'Anonymous';
// Good
const city = user?.address?.city;
const name = user.name ?? 'Anonymous';Use Map and Set When Appropriate
// Use Map for key-value pairs with non-string keys or frequent additions/deletions
const cache = new Map();
cache.set(userId, userData);
cache.has(userId); // true
// Use Set for unique values
const uniqueTags = new Set(['js', 'react', 'js']); // Set(2) {'js', 'react'}
const hasTag = uniqueTags.has('react'); // true
Async Patterns
Use async/await Over Raw Promises
// Bad - promise chains
function getUser(id) {
return fetch(`/api/users/${id}`)
.then((res) => res.json())
.then((user) => fetch(`/api/posts?userId=${user.id}`))
.then((res) => res.json());
}
// Good - async/await
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
return postsRes.json();
}Run Independent Promises in Parallel
// Bad - sequential when they could be parallel
const users = await getUsers();
const posts = await getPosts();
// Good - parallel
const [users, posts] = await Promise.all([getUsers(), getPosts()]);Use Promise.allSettled When Failures Are Acceptable
const results = await Promise.allSettled([
fetchFromPrimary(),
fetchFromBackup(),
fetchFromCache(),
]);
const successes = results
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);Always Handle Async Errors
// Bad - unhandled rejection
async function loadData() {
const res = await fetch('/api/data');
return res.json();
}
// Good - proper error handling
async function loadData() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
console.error('Failed to load data:', error);
return null;
}
}Error Handling
Use Custom Error Classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(message, fields) {
super(message, 400);
this.name = 'ValidationError';
this.fields = fields;
}
}Don't Swallow Errors
// Bad - silently swallows the error
try {
await saveData();
} catch (e) {
// do nothing
}
// Good - handle or rethrow
try {
await saveData();
} catch (error) {
if (error instanceof ValidationError) {
showFieldErrors(error.fields);
} else {
throw error; // Rethrow unexpected errors
}
}Modules
Use ES Modules
// Named exports for utilities
export function formatDate(date) { ... }
export function parseDate(str) { ... }
// Default export for main component/class
export default class UserService { ... }
// Import what you need
import UserService from './user-service.js';
import { formatDate } from './utils.js';Avoid Circular Dependencies
// Bad - A imports B, B imports A
// a.js
import { helperB } from './b.js';
// b.js
import { helperA } from './a.js';
// Good - extract shared logic
// shared.js
export function sharedHelper() { ... }
// a.js
import { sharedHelper } from './shared.js';
// b.js
import { sharedHelper } from './shared.js';Performance
Debounce Expensive Operations
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const handleSearch = debounce((query) => {
fetch(`/api/search?q=${encodeURIComponent(query)}`);
}, 300);
input.addEventListener('input', (e) => handleSearch(e.target.value));Use for...of Over forEach for Performance
// forEach - creates a callback for each iteration
items.forEach((item) => process(item));
// for...of - no callback overhead, supports break/continue
for (const item of items) {
if (item.skip) continue;
process(item);
if (item.last) break;
}Avoid Memory Leaks
// Always clean up event listeners
const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
// Later, clean up all listeners at once
controller.abort();
// Always clean up timers
const intervalId = setInterval(poll, 5000);
// Later
clearInterval(intervalId);Security
Sanitize User Input
// Never insert user input as HTML
// Bad
element.innerHTML = userInput;
// Good
element.textContent = userInput;
// If HTML is needed, sanitize first
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);Use encodeURIComponent for URLs
// Bad - vulnerable to injection
const url = `/search?q=${query}`;
// Good
const url = `/search?q=${encodeURIComponent(query)}`;Avoid eval and Function Constructor
// Never do this
eval(userInput);
new Function(userInput)();
// Use safer alternatives like JSON.parse for data
const data = JSON.parse(jsonString);Modern JavaScript Features
Use Logical Assignment Operators
// Nullish coalescing assignment
user.name ??= 'Anonymous';
// OR assignment
config.debug ||= false;
// AND assignment
user.verified &&= checkVerification();Use structuredClone for Deep Copies
// Bad - doesn't handle nested objects
const copy = { ...original };
// Bad - slow and loses special types
const copy = JSON.parse(JSON.stringify(original));
// Good - native deep clone
const copy = structuredClone(original);Use Array.at() for Negative Indexing
const items = [1, 2, 3, 4, 5];
// Old way
const last = items[items.length - 1]; // 5
// Modern way
const last = items.at(-1); // 5
const secondLast = items.at(-2); // 4
Quick Reference
| Practice | Why |
|---|---|
const by default | Prevents accidental reassignment |
| Descriptive names | Self-documenting code |
| Early returns | Reduces nesting and complexity |
| Destructuring | Cleaner variable extraction |
async/await | Readable async code |
Promise.all for parallel work | Faster execution |
| Custom error classes | Structured error handling |
| ES modules | Tree-shaking, static analysis |
structuredClone | Safe deep copies |
| Sanitize user input | Prevent XSS attacks |
Summary
Writing good JavaScript comes down to:
- Be explicit — descriptive names,
const/let, early returns - Be immutable — spread operators,
map/filterover mutation - Be async-aware —
async/await, parallel promises, proper error handling - Be secure — sanitize input, encode URLs, avoid
eval - Be modern — use the latest language features for cleaner code
Building with a framework? Read Building Production-Ready Apps with Next.js and Understanding React Server Components to see how these fundamentals apply in practice.