JavaScript is the world's most widely deployed programming language — and one of the easiest to write badly. Its flexibility is both its greatest strength and its most dangerous quality. Without discipline and a set of agreed-upon best practices, JavaScript codebases quickly become unmaintainable. This guide covers the practices that separate professional JavaScript from amateur code.
1. Use const by Default, let When Necessary, Never var
The var keyword has function scope and is hoisted to the top of its function, which leads to subtle bugs. Always use const for values that don't change and let for values that do. This makes your code's intent explicit and eliminates a whole class of scoping bugs:
// ❌ Avoid: var is function-scoped and hoisted var count = 0; // ✅ Use const for values that won't be reassigned const MAX_RETRIES = 3; const apiUrl = 'https://api.example.com/v1'; // ✅ Use let only when you need to reassign let currentPage = 1; currentPage += 1;
2. Embrace Modern ES6+ Syntax
Modern JavaScript (ES6 and beyond) offers cleaner, more expressive syntax. Use it consistently:
- Arrow functions —
const double = x => x * 2;— shorter and lexically bindthis. - Template literals —
`Hello, ${name}!`— avoid string concatenation. - Destructuring —
const { id, name } = user;— extract properties cleanly. - Spread/rest operators —
const merged = { ...defaults, ...overrides }; - Optional chaining —
user?.address?.city— safely access nested properties. - Nullish coalescing —
const port = config.port ?? 3000;— use a default only when the value isnullorundefined.
3. Always Handle Errors — Don't Swallow Them
Unhandled errors are one of the most common sources of silent bugs in JavaScript applications. Never swallow errors with an empty catch block:
// ❌ Swallowing errors — worst possible practice
try {
const data = JSON.parse(rawInput);
} catch (e) {}
// ✅ Handle errors meaningfully
try {
const data = JSON.parse(rawInput);
processData(data);
} catch (error) {
console.error('Failed to parse JSON input:', error.message);
showUserFriendlyError('Invalid data format. Please check your input.');
}
4. Use async/await Over Raw Promises
The async/await syntax makes asynchronous code read like synchronous code, dramatically improving readability and making error handling with try/catch natural:
// ❌ Promise chains — hard to read and error-prone
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(res => res.json())
.then(posts => renderPosts(posts))
.catch(err => console.error(err));
// ✅ async/await — clean and readable
async function loadUserPosts() {
try {
const userRes = await fetch('/api/user');
const user = await userRes.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
renderPosts(posts);
} catch (error) {
console.error('Failed to load posts:', error);
}
}
5. Write Pure Functions Where Possible
A pure function always returns the same output for the same input and has no side effects. Pure functions are trivial to test, easy to reason about, and safe to use anywhere:
// ❌ Impure — modifies external state
let total = 0;
function addToTotal(amount) { total += amount; }
// ✅ Pure — no side effects, predictable output
function add(a, b) { return a + b; }
const total = [10, 20, 30].reduce(add, 0);
6. Avoid Mutating Objects and Arrays Directly
Mutating data in place causes bugs that are extremely hard to track down, especially in frameworks like React where immutability is required for correct rendering. Use immutable update patterns:
// ❌ Mutating — unpredictable in shared state
const user = { name: 'Alice', role: 'user' };
user.role = 'admin'; // Mutates original!
// ✅ Create a new object instead
const updatedUser = { ...user, role: 'admin' };
// ❌ Mutating an array
items.push(newItem);
// ✅ Immutable array update
const newItems = [...items, newItem];
7. Use Strict Equality (===) Not Loose Equality (==)
JavaScript's loose equality operator (==) performs type coercion, which leads to surprising results like 0 == false being true and "" == false being true. Always use strict equality (===) which compares both value and type:
// ❌ Loose equality — surprising results 0 == false // true! "" == false // true! null == undefined // true! // ✅ Strict equality — no surprises 0 === false // false "" === false // false null === undefined // false
8. Debounce and Throttle Expensive Event Handlers
Event listeners on scroll, resize, input, and mousemove can fire hundreds of times per second. Running expensive operations (like DOM manipulation or API calls) in these handlers without rate-limiting causes performance degradation and poor user experience:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Search only fires 300ms after the user stops typing
const handleSearch = debounce(async (query) => {
const results = await searchAPI(query);
renderResults(results);
}, 300);
input.addEventListener('input', e => handleSearch(e.target.value));
9. Minify JavaScript for Production
Minified JavaScript is smaller (fewer bytes to download), parses faster, and executes faster than formatted code. Modern bundlers like Vite and Webpack handle this automatically. If you're managing scripts manually, use our free JavaScript Minifier to compress your scripts. When you need to read or debug minified code from a third party, use our JavaScript Beautifier to restore indentation and formatting.
10. Document Non-Obvious Code with Comments
Good code is self-documenting — variable names, function names, and structure should communicate intent. But some logic is inherently non-obvious: a workaround for a browser bug, a business rule encoded in a formula, or a performance optimisation that looks incorrect at first glance. These must be commented:
// ❌ Useless comment — the code already says this
// Add one to count
count++;
// ✅ Explains the "why" — which the code can't
// iOS Safari fires both 'touchend' and 'click' on interactive elements.
// We stop propagation on touchend to prevent the click handler from
// firing twice and submitting the form a second time.
button.addEventListener('touchend', (e) => {
e.stopPropagation();
submitForm();
});
Minify or Beautify Your JavaScript
Compress JS for production or expand minified third-party code for debugging — free, instant, no sign-up.