JavaScript Best Practices
Most JavaScript bugs are not exotic — they come from loose equality, accidental mutation, forgotten await, and unclear naming. Modern JavaScript (ES2020+) gives you the tools to sidestep nearly all of them, and a handful of consistent habits keep a codebase readable as it grows. This page collects the practices that matter most, grouped by area, with the reasoning behind each one so you can apply them judgement-first rather than as cargo-cult rules.
Variables and declarations
Default to const. Use let only when you genuinely reassign, and avoid var entirely — its function scope and hoisting cause surprises that block scope eliminates. A const binding signals intent: “this name will not be reassigned,” which makes code easier to scan.
const MAX_RETRIES = 3; // constant, never reassigned
let attempts = 0; // changes over time
for (const user of users) { // fresh binding each iteration
// ...
}
constfreezes the binding, not the value.const arr = []; arr.push(1)is legal — reach forObject.freezeor immutable updates when you need the data itself protected.
Equality and types
Always use === and !==. Loose equality coerces operands and produces results like 0 == "" being true. The one sanctioned use of == is the deliberate value == null check, which matches both null and undefined.
if (input === "") { /* exact empty string */ }
if (value == null) { /* null OR undefined */ }
Prefer explicit conversion (Number(x), String(x), Boolean(x)) over relying on implicit coercion inside expressions.
Functions
Keep functions small and single-purpose, and favor pure functions — same input, same output, no side effects — because they are trivial to test and reason about. Use arrow functions for callbacks to preserve lexical this, and regular methods for object behavior that needs its own this.
// Pure: easy to test, no hidden state
const total = (items) => items.reduce((sum, i) => sum + i.price, 0);
// Default parameters beat ||-based defaults
function greet(name = "friend") {
return `Hi, ${name}`;
}
Use default and rest parameters instead of reading arguments, and prefer named exports so tooling can rename and tree-shake reliably.
Asynchronous code
Prefer async/await over long .then() chains — it reads top to bottom and integrates with try/catch. Run independent async work in parallel with Promise.all rather than awaiting each call in sequence.
// Sequential (slow) — each await waits for the previous
const a = await getA();
const b = await getB();
// Parallel (fast) — both start immediately
const [a2, b2] = await Promise.all([getA(), getB()]);
Always handle rejections: wrap awaits in try/catch, and never leave a promise floating without a .catch or surrounding handler.
Immutability and data
Avoid mutating data you do not own — especially function arguments and shared state. Update arrays and objects by producing new copies with spread, map, and filter, or use the non-mutating array methods (toSorted, toReversed, with).
const updated = { ...user, name: "Ada" }; // new object
const next = items.filter((i) => i.id !== id); // new array
const sorted = scores.toSorted((a, b) => b - a); // original untouched
For deep copies, use structuredClone rather than the fragile JSON.parse(JSON.stringify(...)) trick, which silently drops functions, Date precision, and undefined.
Objects and safe access
Use optional chaining (?.) and nullish coalescing (??) to read possibly-missing data without long && guards — and without the falsy-value bugs of ||.
const city = user?.address?.city ?? "Unknown";
const pageSize = options.size ?? 20; // keeps 0; || would replace it
Modules and structure
Organize code into ES modules with explicit import/export. Keep each module focused, avoid attaching anything to the global scope, and put related logic together. Static imports let bundlers tree-shake; reserve dynamic import() for genuine code-splitting.
Naming and readability
| Kind | Convention | Example |
|---|---|---|
| Variables, functions | camelCase | userCount, fetchData |
| Classes, constructors | PascalCase | UserProfile |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES |
| Private class members | # prefix | #secret |
| Booleans | is/has/can prefix | isActive, hasAccess |
Choose descriptive names over comments where possible, and use comments to explain why, not what.
Best Practices
- Default to
const, useletonly to reassign, and never usevar. - Always use strict equality (
===); reserve== nullfor the intentional null-or-undefined check. - Write small, pure functions and keep side effects at the edges of your code.
- Use
async/awaitwithtry/catch, and parallelize independent work withPromise.all. - Treat data as immutable: copy with spread/
map/filterand deep-copy withstructuredClone. - Reach for
?.and??to access optional data safely without falsy-value bugs. - Keep code in focused ES modules, avoid globals, and name things clearly and consistently.