Skip to content
JavaScript best practices 4 min read

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
  // ...
}

const freezes the binding, not the value. const arr = []; arr.push(1) is legal — reach for Object.freeze or 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

KindConventionExample
Variables, functionscamelCaseuserCount, fetchData
Classes, constructorsPascalCaseUserProfile
ConstantsUPPER_SNAKE_CASEMAX_RETRIES
Private class members# prefix#secret
Booleansis/has/can prefixisActive, hasAccess

Choose descriptive names over comments where possible, and use comments to explain why, not what.

Best Practices

  • Default to const, use let only to reassign, and never use var.
  • Always use strict equality (===); reserve == null for the intentional null-or-undefined check.
  • Write small, pure functions and keep side effects at the edges of your code.
  • Use async/await with try/catch, and parallelize independent work with Promise.all.
  • Treat data as immutable: copy with spread/map/filter and deep-copy with structuredClone.
  • 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.
Last updated June 1, 2026
Was this helpful?