JavaScript Best Practices
JavaScript Best Practices
Working code is the starting line, not the finish. The practices below are the difference between code that merely runs and code that teammates can read, extend, and trust six months later. Each one includes the why, because a rule you understand is a rule you can apply in new situations.
Step 1: Name Things for the Reader
// Bad — the reader must decode
const x = users.filter(u => u.a > 18);
// Good — the code explains itself
const adultUsers = users.filter(user => user.age > 18);
Why: code is read ten times more often than it is written. A good name eliminates a comment, an investigation, sometimes a bug. Conventions that pay off immediately: booleans read as questions (isActive, hasPermission), functions start with verbs (fetchUser, validateEmail), and a name you struggle to write usually means the function does too much.
Step 2: Keep Functions Small and Single-Purpose
// Bad — one function doing four jobs
function processUser(user) {
// validate, transform, save, notify... 80 lines
}
// Good — each piece testable and reusable
function validateUser(user) { /* ... */ }
function normalizeUser(user) { /* ... */ }
function saveUser(user) { /* ... */ }
function notifyUser(user) { /* ... */ }
Why: small functions localize failure (the stack trace names the culprit), enable unit testing without elaborate setup, and turn complex flows into a readable sequence of verbs. If you cannot name a function without "and", split it.
Step 3: Use Early Returns Instead of Nesting
// Bad — logic buried three levels deep
function getDiscount(user) {
if (user) {
if (user.isPremium) {
if (user.years > 5) {
return 0.3;
}
return 0.2;
}
return 0.1;
}
return 0;
}
// Good — handle edge cases and move on
function getDiscount(user) {
if (!user) return 0;
if (!user.isPremium) return 0.1;
if (user.years <= 5) return 0.2;
return 0.3;
}
Why: each early return removes one condition from your mental stack. The happy path reads straight down the left margin, and every edge case is dealt with exactly where it is detected.
Step 4: Prefer Strict Equality and const
0 == ""; // true — coercion surprises
0 === ""; // false — what you meant
const items = []; // binding cannot be reassigned
items.push("ok"); // contents can still change
Why: === never converts types, so comparisons mean what they say. const-by-default documents intent — any let becomes a signal saying "this value changes, watch it".
Step 5: Two Patterns Worth Memorizing
Debounce — collapse a burst of events into one call after the burst ends:
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const search = debounce((query) => fetchResults(query), 300);
// typing "hello" fires ONE request, not five
Why it works: each keystroke cancels the previous timer and starts a new one; only the final keystroke's timer survives long enough to fire. The closure over timeoutId is what remembers state between calls.
Memoize — cache pure function results:
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
Why it works: identical inputs to a pure function always produce identical outputs, so the second call can skip computing. Only memoize genuinely pure, genuinely expensive functions — the cache is memory you are trading for speed.
Step 6: Security Non-Negotiables
// Never execute strings as code
eval(userInput); // NEVER — arbitrary code execution
// Never inject user input as HTML
element.innerHTML = userInput; // XSS risk
element.textContent = userInput; // safe — always plain text
// If you truly need user HTML, sanitize it first
const clean = DOMPurify.sanitize(userInput);
Why: anything a user typed must be treated as data, never as code or markup. One innerHTML with unsanitized input lets an attacker run scripts as your users — stealing sessions, posting as them. textContent is immune by construction.
Common Beginner Errors and Fixes
- Mutating shared objects: a function edits the array it received and surprises every other holder of that array. Fix: return new values —
[...arr, item],{ ...obj, key }. - Magic numbers:
if (status === 3)means nothing next month. Fix:const STATUS_SHIPPED = 3;or an object of named constants. - Dead code kept "just in case": commented-out blocks rot and mislead. Fix: delete it — version control remembers.
- Catch-all error handling:
catch (e) {}hides real failures. Fix: log, handle what you can, rethrow what you cannot. - Premature optimization: clever micro-tuned code that saved nothing measurable. Fix: write clear code first; optimize only what profiling proves slow.
Practice Exercise
Refactor this deliberately messy function using at least four practices from this tutorial:
function d(u, t) {
if (u != null) {
if (u.p == true) {
var r = t - t * 0.2;
return r;
} else {
var r2 = t - t * 0.05;
return r2;
}
} else {
return t;
}
}
!=/== with strict equivalents and var with const.memoizedTotal = memoize(yourFunction) and confirm repeated calls with the same arguments hit the cache (add a console.log inside to see it skip).Compare your result with the original: same behavior, but now a stranger — or you in six months — understands it in five seconds. That is the entire point of best practices.