|$ curl https://forge-ai.dev/api/markdown?path=docs/js/best-practices
$cat docs/javascript-—-best-practices-&-patterns.md
updated Recently·22 min read·published

JavaScript — Best Practices & Patterns

JavaScriptAdvanced🎯Free Tools
Introduction

JavaScript powers everything from small UI interactions to full-stack applications. Its flexible, dynamic nature makes it easy to start and hard to master. Writing maintainable JavaScript requires discipline: consistent style, predictable async flow, defensive error handling, modular architecture, and continuous testing.

This guide distills community-agreed best practices — from modern syntax and module design to performance, security, and TypeScript migration. Use it as a reference for code reviews, onboarding, and leveling up a codebase from "works" to "production-ready."

js_motto.js
JavaScript
1// A single source of truth beats clever code.
2// Prefer clarity over brevity, explicit over implicit,
3// and composition over inheritance.
4
5const CONFIG = Object.freeze({
6 apiBase: "https://api.example.com/v1",
7 retries: 3,
8 timeout: 5000,
9});
10
11async function fetchUser(id) {
12 const url = new URL(`/users/${id}`, CONFIG.apiBase);
13 const res = await fetch(url, { signal: AbortSignal.timeout(CONFIG.timeout) });
14 if (!res.ok) throw new Error(`HTTP ${res.status}`);
15 return res.json();
16}
Dos and Don'ts

These rules-of-thumb cover the most common mistakes and recommended alternatives. Internalize the "why" behind each one, because every rule has exceptions in the right context.

AvoidPreferRationale
varconst / letBlock scoping prevents hoisting bugs and accidental globals
=====Strict equality avoids surprising type coercion
for...in on arraysfor...of / array methodsIndexes are strings; prototype properties may leak
callback pyramidsasync / awaitLinear control flow is easier to read and debug
silent catchspecific error handling + loggingEmpty catch blocks hide failures
modifying argumentspure functions / return new valuesPredictable outputs simplify testing and reasoning
global variablesmodules / closuresNamespaces reduce collision risk in large codebases
JSON.parse untrusted datavalidation + schema checksPrevents injection and runtime type errors
magic numbersnamed constantsIntent is obvious and changes are centralized
deep inheritance treescomposition + factory functionsFlatter hierarchies reduce coupling
dos_donts.js
JavaScript
1// Bad — var, ==, silent catch, magic number
2function load(id) {
3 var url = "https://api.example.com/items/" + id;
4 return fetch(url)
5 .then(r => r.json())
6 .catch(() => {}); // failure swallowed
7}
8
9// Good — const, ===, explicit errors, named constants
10const API_BASE = "https://api.example.com";
11const TIMEOUT_MS = 5000;
12
13async function loadItem(id) {
14 const url = new URL(`/items/${id}`, API_BASE);
15 const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
16 if (!res.ok) throw new Error(`Failed to load item ${id}: ${res.status}`);
17 return res.json();
18}

warning

Avoid == null unless you intentionally want to treat both null and undefined as equivalent. In most cases, prefer === and be explicit about which falsy values your code accepts.
Code Style & Conventions

Consistent style reduces cognitive load. Use a formatter (Prettier) and a linter (ESLint) so code reviews focus on design, not whitespace. Adopt a published style guide such as Airbnb, Standard, or the Google JavaScript Style Guide, then configure tooling to enforce it.

ConventionRecommendation
VariablescamelCase for variables/functions, PascalCase for classes, UPPER_SNAKE_CASE for constants
DeclarationDefault to const; use let only when rebinding is required; never use var
QuotesPick single or double and stay consistent; use backticks for interpolation
SemicolonsUse them explicitly or rely on ASI consistently — do not mix
Line length80–100 characters; break long chains and parameter lists
Trailing commasAlways in multi-line objects/arrays/params for cleaner diffs
style_example.js
JavaScript
1// Good style example
2const MAX_RESULTS = 100;
3
4class UserRepository {
5 constructor(apiClient) {
6 this.apiClient = apiClient;
7 }
8
9 async findById(id) {
10 const data = await this.apiClient.get(`/users/${id}`);
11 return UserRepository.normalize(data);
12 }
13
14 static normalize(raw) {
15 return {
16 id: raw.id,
17 name: raw.name ?? "Anonymous",
18 email: raw.email?.toLowerCase(),
19 };
20 }
21}
22
23export { UserRepository };

info

Configure .editorconfig alongside Prettier and ESLint so every contributor uses the same indentation, line endings, and final newline. Run formatting and linting in a pre-commit hook so bad code never reaches review.
Modern Syntax & Patterns

Modern JavaScript (ES2015+) removes boilerplate and adds safer primitives. Use destructuring, rest/spread, optional chaining, nullish coalescing, and template literals to make code more readable and less error-prone.

modern_syntax.js
JavaScript
1// Destructuring with defaults
2function renderCard({ title, description = "", tags = [] }) {
3 return `<article>
4 <h2>${title}</h2>
5 <p>${description}</p>
6 ${tags.map(t => `<span>${t}</span>`).join("")}
7</article>`;
8}
9
10// Rest/spread for immutability
11const user = { id: 1, name: "Ada" };
12const updated = { ...user, name: "Ada Lovelace", lastLogin: new Date() };
13const { lastLogin, ...rest } = updated;
14
15// Optional chaining + nullish coalescing
16const city = user?.address?.city ?? "Unknown";
17const count = response?.meta?.count ?? 0; // 0 is preserved, unlike ||
18
19// Array helpers
20const active = users.filter(u => u.isActive);
21const names = active.map(u => u.name);
22const total = cart.reduce((sum, item) => sum + item.price, 0);
23
24// Short-circuit for side-effect guards
25isReady && startApp();

When transforming data, prefer immutable operations. They prevent hidden mutations and make React/Vue state updates predictable.

immutability.js
JavaScript
1// Bad — mutates the original array
2function markDone(tasks) {
3 tasks.forEach(t => { t.done = true; });
4 return tasks;
5}
6
7// Good — returns a new array
8function markAllDone(tasks) {
9 return tasks.map(t => ({ ...t, done: true }));
10}
11
12// Even better — pure function with explicit predicate
13function toggleTask(tasks, id) {
14 return tasks.map(t =>
15 t.id === id ? { ...t, done: !t.done } : t
16 );
17}
Async & Error Handling

Async JavaScript is where most production bugs hide. Always await failures explicitly, avoid unhandled Promise rejections, and use AbortController to cancel in-flight requests.

async_patterns.js
JavaScript
1// Bad — unhandled rejection, no timeout
2function getData() {
3 fetch("/api/data").then(res => res.json());
4}
5
6// Good — structured async with error boundaries
7async function getData(signal) {
8 try {
9 const res = await fetch("/api/data", { signal });
10 if (!res.ok) throw new Error(`HTTP ${res.status}`);
11 return await res.json();
12 } catch (err) {
13 if (err.name === "AbortError") return null;
14 console.error("Failed to load data", err);
15 throw err;
16 }
17}
18
19// Parallel execution with bounded concurrency
20async function loadUsers(ids) {
21 const batch = ids.slice(0, 10); // limit concurrency
22 const users = await Promise.all(
23 batch.map(id => fetchUser(id))
24 );
25 return users;
26}

best practice

Use Promise.all only when failures in any task should fail the whole operation. Use Promise.allSettled when you need results from every task regardless of individual failures. Avoid Promise.race unless you truly need the first settlement.
retry_cancel.js
JavaScript
1// Retry with exponential backoff
2async function fetchWithRetry(url, options = {}, maxRetries = 3) {
3 let attempt = 0;
4 while (attempt <= maxRetries) {
5 try {
6 const res = await fetch(url, options);
7 if (res.ok) return res;
8 throw new Error(`HTTP ${res.status}`);
9 } catch (err) {
10 if (attempt === maxRetries) throw err;
11 const delay = 2 ** attempt * 100;
12 await new Promise(r => setTimeout(r, delay));
13 attempt++;
14 }
15 }
16}
17
18// Request cancellation
19const controller = new AbortController();
20fetchUser(1, controller.signal);
21controller.abort(); // stops the fetch cleanly
Modules & Architecture

ES modules provide static analysis, tree shaking, and clear dependencies. Prefer named exports for most modules, keep side effects out of module scope, and avoid circular dependencies by extracting shared code into a third module.

modules.js
JavaScript
1// utils/date.js — focused module with named exports
2export function formatDate(date, locale = "en-US") {
3 return new Intl.DateTimeFormat(locale).format(date);
4}
5
6export function parseISO(value) {
7 const d = new Date(value);
8 if (Number.isNaN(d.getTime())) throw new RangeError("Invalid date");
9 return d;
10}
11
12// services/api.js — default export for a configured client
13import { API_BASE } from "../config.js";
14
15export default async function api(path, options = {}) {
16 const res = await fetch(`${API_BASE}${path}`, {
17 headers: { "Content-Type": "application/json" },
18 ...options,
19 });
20 if (!res.ok) throw new Error(`API ${res.status}`);
21 return res.json();
22}
23
24// Avoid side effects at import time
25// Bad: fetch("/config") at top level makes the module order-dependent
26// Good: export a function that callers invoke explicitly
27export async function loadConfig() { /* ... */ }

Organize modules by feature, not by layer. A feature folder groups components, utilities, and tests together, making ownership and refactoring easier.

feature_module.js
JavaScript
1// Feature-based folder
2// src/features/cart/
3// Cart.js
4// cartStore.js
5// cartApi.js
6// cartUtils.js
7// cart.test.js
8
9// cart/cartUtils.js
10export function calculateTotals(items, discount = 0) {
11 const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
12 const total = Math.max(0, subtotal - discount);
13 return { subtotal, discount, total };
14}
15
16export function validateItem(item) {
17 return (
18 item &&
19 typeof item.price === "number" &&
20 item.price >= 0 &&
21 Number.isInteger(item.qty) &&
22 item.qty > 0
23 );
24}
Project Structure

A well-organized project separates public API surface from implementation details. Keep configuration at the root, source code under src/, and tests next to the code they exercise or under __tests__.

project_structure.txt
TEXT
1my-app/
2├── public/ # static assets
3├── src/
4│ ├── main.js # application entry point
5│ ├── config.js # environment-derived constants
6│ ├── lib/ # framework-agnostic utilities
7│ ├── components/ # reusable UI components
8│ ├── features/ # domain-specific modules
9│ ├── services/ # external API clients
10│ └── styles/ # global styles
11├── tests/ # integration / e2e tests
12├── package.json
13├── eslint.config.js
14├── prettier.config.js
15└── README.md

Use absolute or alias imports to avoid fragile relative paths. Most bundlers support @/ aliases via configuration, which keeps imports stable during moves.

aliases.js
JavaScript
1// Fragile relative import
2import { formatDate } from "../../../utils/date.js";
3
4// Stable alias import
5import { formatDate } from "@/utils/date.js";
6
7// Vite alias example (vite.config.js)
8// export default {
9// resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
10// };
11
12// Node ESM subpath imports (package.json)
13// "imports": {
14// "#root/*": "./src/*"
15// }
16// import { formatDate } from "#root/utils/date.js";
📝

note

Avoid index.js barrel files that re-export everything. They defeat tree shaking, increase bundle size, and create circular-import hazards. Import directly from the file that defines the symbol.
Performance

JavaScript performance is usually about the DOM, network, and memory rather than raw execution speed. Measure before optimizing, focus on user-centric metrics, and avoid micro-optimizations that hurt readability.

performance.js
JavaScript
1// Debounce — wait for a pause in events
2function debounce(fn, wait) {
3 let timer;
4 return function (...args) {
5 clearTimeout(timer);
6 timer = setTimeout(() => fn.apply(this, args), wait);
7 };
8}
9window.addEventListener("resize", debounce(handleResize, 200));
10
11// Throttle — run at most once per period
12function throttle(fn, limit) {
13 let inThrottle;
14 return function (...args) {
15 if (!inThrottle) {
16 fn.apply(this, args);
17 inThrottle = true;
18 setTimeout(() => (inThrottle = false), limit);
19 }
20 };
21}
22window.addEventListener("scroll", throttle(handleScroll, 100));
23
24// Efficient DOM batching
25const fragment = document.createDocumentFragment();
26items.forEach(item => {
27 const li = document.createElement("li");
28 li.textContent = item.name;
29 fragment.appendChild(li);
30});
31list.appendChild(fragment); // one reflow

Memory leaks often come from forgotten event listeners, timers, and closures over DOM nodes. Clean up in teardown hooks or when components unmount.

cleanup.js
JavaScript
1// Cleanup pattern for long-lived listeners
2let abortController;
3
4function subscribe(channel) {
5 abortController = new AbortController();
6 eventBus.addEventListener(channel, handler, { signal: abortController.signal });
7}
8
9function unsubscribe() {
10 abortController?.abort(); // removes all listeners tied to this signal
11}
12
13// Alternative: explicit removeEventListener
14function setup() {
15 window.addEventListener("keydown", onKeyDown);
16 return () => window.removeEventListener("keydown", onKeyDown);
17}

info

Use requestIdleCallback or scheduler.yield() for non-urgent background work. For smooth animations, use requestAnimationFrame and avoid mutating layout properties inside the loop.
Security

JavaScript runs in hostile environments. Treat all user input as untrusted, avoid dangerous APIs, and use built-in sanitization primitives before inserting content into the DOM.

security.js
JavaScript
1// XSS-safe DOM insertion — never use innerHTML with user data
2const userInput = "<img src=x onerror=alert('xss')>";
3
4// Safe: textContent escapes HTML
5const el = document.createElement("p");
6el.textContent = userInput;
7container.appendChild(el);
8
9// Safe: use a sanitizer when HTML is required
10import DOMPurify from "dompurify";
11container.innerHTML = DOMPurify.sanitize(userInput);
12
13// Avoid eval and new Function
14// Bad
15const result = eval(userCode);
16
17// Good — use a parser or structured data
18const config = JSON.parse(userJson);
19
20// URL validation before redirecting
21function safeRedirect(path) {
22 const allowed = ["/home", "/profile", "/settings"];
23 return allowed.includes(path) ? path : "/";
24}

For secrets and environment-specific values, never expose them in client-side bundles. Use environment variables at build time for public values only, and keep API keys and database credentials on the server.

security_env.js
JavaScript
1// Vite public env (safe to expose)
2const apiUrl = import.meta.env.VITE_API_URL;
3
4// Node private env (server only)
5const dbPassword = process.env.DB_PASSWORD;
6
7// Content Security Policy via meta tag or headers
8// <meta http-equiv="Content-Security-Policy"
9// content="default-src 'self'; script-src 'self';">
10
11// Strict validation example
12function validateEmail(value) {
13 if (typeof value !== "string" || value.length > 254) return false;
14 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
15}

danger

Never execute user-supplied strings with eval(), new Function(), or setTimeout(string). These are persistent XSS vectors and bypass most content security policies.
Testing & Debugging

Reliable JavaScript is tested JavaScript. Write unit tests for pure functions, integration tests for API clients, and end-to-end tests for critical user flows. Aim for high confidence, not just high coverage.

testing.js
JavaScript
1// Pure function unit test (Vitest/Jest syntax)
2import { describe, it, expect } from "vitest";
3import { calculateTotals } from "./cartUtils.js";
4
5describe("calculateTotals", () => {
6 it("applies a discount correctly", () => {
7 const items = [{ price: 50, qty: 2 }, { price: 30, qty: 1 }];
8 expect(calculateTotals(items, 20)).toEqual({
9 subtotal: 130,
10 discount: 20,
11 total: 110,
12 });
13 });
14
15 it("never returns a negative total", () => {
16 expect(calculateTotals([{ price: 5, qty: 1 }], 100).total).toBe(0);
17 });
18});
19
20// Async integration test with mocked fetch
21import { vi } from "vitest";
22
23globalThis.fetch = vi.fn(() =>
24 Promise.resolve({
25 ok: true,
26 json: () => Promise.resolve({ id: 1, name: "Ada" }),
27 })
28);
29
30it("fetches a user", async () => {
31 const user = await fetchUser(1);
32 expect(user.name).toBe("Ada");
33});

Keep tests deterministic. Avoid relying on timers, randomness, or network state unless explicitly mocked. Use fakeTimers for time-dependent code and reset mocks between tests.

debugging.js
JavaScript
1// Debugging — structured logs over console.log
2function handleSubmit(data) {
3 console.group("handleSubmit");
4 console.log("payload:", data);
5 console.trace("caller");
6 console.groupEnd();
7}
8
9// Conditional breakpoints in DevTools
10// Right-click a line → "Add conditional breakpoint" → state.count > 100
11
12// Use debugger statement sparingly in source
13function complexLogic(state) {
14 if (state.invalid) debugger; // pauses in DevTools
15 return reconcile(state);
16}
Accessibility

JavaScript is responsible for much of the interactivity that makes or breaks accessibility. Manage focus, preserve keyboard operability, and announce dynamic changes to assistive technologies.

a11y.js
JavaScript
1// Focus management after modal opens
2function openModal() {
3 modal.hidden = false;
4 previouslyFocused = document.activeElement;
5 closeButton.focus();
6}
7
8function closeModal() {
9 modal.hidden = true;
10 previouslyFocused?.focus();
11}
12
13// Keyboard trap inside a modal
14trap.addEventListener("keydown", (e) => {
15 if (e.key !== "Tab") return;
16 const focusables = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
17 const first = focusables[0];
18 const last = focusables[focusables.length - 1];
19
20 if (e.shiftKey && document.activeElement === first) {
21 e.preventDefault();
22 last.focus();
23 } else if (!e.shiftKey && document.activeElement === last) {
24 e.preventDefault();
25 first.focus();
26 }
27});
28
29// Announce status changes to screen readers
30const liveRegion = document.createElement("div");
31liveRegion.setAttribute("role", "status");
32liveRegion.setAttribute("aria-live", "polite");
33liveRegion.className = "sr-only";
34document.body.appendChild(liveRegion);
35
36function announce(message) {
37 liveRegion.textContent = message;
38}

best practice

If an element is clickable via JavaScript, it should also be focusable and activatable with Enter/Space. Prefer native <button> elements over <div> handlers. When you must use a generic element, add tabindex="0", a visible focus style, and keyboard listeners.
Tooling

Modern JavaScript development depends on a reliable toolchain. A formatter, linter, type checker, test runner, and bundler are the minimum viable setup for any non-trivial project.

package.json
JSON
1{
2 "type": "module",
3 "scripts": {
4 "dev": "vite",
5 "build": "vite build",
6 "preview": "vite preview",
7 "test": "vitest",
8 "test:run": "vitest run",
9 "lint": "eslint .",
10 "format": "prettier --write .",
11 "typecheck": "tsc --noEmit"
12 },
13 "devDependencies": {
14 "eslint": "^9.0.0",
15 "prettier": "^3.0.0",
16 "typescript": "^5.4.0",
17 "vitest": "^2.0.0",
18 "vite": "^5.0.0"
19 }
20}

Pin tool versions and run them in CI. A failing lint or type check should block merges. Use npm scripts as the single source of truth so local and CI commands stay in sync.

ci.yml
YAML
1# .github/workflows/ci.yml
2name: CI
3on: [push, pull_request]
4jobs:
5 check:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-node@v4
10 with:
11 node-version: 20
12 cache: npm
13 - run: npm ci
14 - run: npm run lint
15 - run: npm run typecheck
16 - run: npm run test:run

info

Use lint-staged with husky to run formatters and linters only on changed files. This keeps pre-commit hooks fast and avoids frustrating delays.
TypeScript Migration

TypeScript is the most effective tool for preventing whole classes of JavaScript bugs. You do not need a big-bang rewrite. Start by enabling allowJs and checkJs, then gradually convert files from the leaves of your dependency tree inward.

tsconfig.json
JSON
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
6 "strict": true,
7 "noImplicitAny": true,
8 "strictNullChecks": true,
9 "noUncheckedIndexedAccess": true,
10 "esModuleInterop": true,
11 "skipLibCheck": true,
12 "forceConsistentCasingInFileNames": true,
13 "allowJs": true,
14 "checkJs": true,
15 "outDir": "./dist",
16 "rootDir": "./src"
17 },
18 "include": ["src/**/*"]
19}
ts_migration.ts
TypeScript
1// Gradually add JSDoc types before full conversion
2/**
3 * @param {string} id
4 * @returns {Promise<{ id: string; name: string }>}
5 */
6export async function fetchUser(id) {
7 const res = await fetch(`/api/users/${id}`);
8 if (!res.ok) throw new Error(String(res.status));
9 return res.json();
10}
11
12// Full TypeScript conversion
13export async function fetchUserTyped(
14 id: string
15): Promise<{ id: string; name: string }> {
16 const res = await fetch(`/api/users/${id}`);
17 if (!res.ok) throw new Error(String(res.status));
18 return res.json();
19}
Code Review Checklist

A checklist keeps reviews consistent and thorough. Automate mechanical checks with CI so reviewers can focus on architecture, correctness, and edge cases.

CheckAutomated?Details
FormattingPrettierConsistent quotes, spacing, and line breaks
LintingESLintUnused vars, unsafe patterns, complexity rules
TypesTypeScriptStrict mode enabled, no implicit any
TestsVitest/JestNew logic covered, existing tests pass
Error handlingReviewAsync errors caught, user-facing messages safe
SecurityESLint / reviewNo eval, sanitized DOM insertion, validated inputs
PerformanceReviewNo unnecessary re-renders or network requests
Accessibilityaxe / reviewKeyboard support, focus management, ARIA labels
NamingReviewClear, intention-revealing names
Bundle sizesize-limit / reviewNo heavy dependencies added without justification
ci_checklist.sh
Bash
1# Run the full local CI pipeline before pushing
2npm run format -- --check
3npm run lint
4npm run typecheck
5npm run test:run
6npm run build
Resources

These references are maintained by the JavaScript community and browser vendors. Keep them bookmarked for authoritative answers and evolving standards.

$Blueprint — Engineering Documentation·Section ID: JS-BP·Revision: 1.0