JavaScript — Best Practices & Patterns
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."
| 1 | // A single source of truth beats clever code. |
| 2 | // Prefer clarity over brevity, explicit over implicit, |
| 3 | // and composition over inheritance. |
| 4 | |
| 5 | const CONFIG = Object.freeze({ |
| 6 | apiBase: "https://api.example.com/v1", |
| 7 | retries: 3, |
| 8 | timeout: 5000, |
| 9 | }); |
| 10 | |
| 11 | async 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 | } |
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.
| Avoid | Prefer | Rationale |
|---|---|---|
| var | const / let | Block scoping prevents hoisting bugs and accidental globals |
| == | === | Strict equality avoids surprising type coercion |
| for...in on arrays | for...of / array methods | Indexes are strings; prototype properties may leak |
| callback pyramids | async / await | Linear control flow is easier to read and debug |
| silent catch | specific error handling + logging | Empty catch blocks hide failures |
| modifying arguments | pure functions / return new values | Predictable outputs simplify testing and reasoning |
| global variables | modules / closures | Namespaces reduce collision risk in large codebases |
| JSON.parse untrusted data | validation + schema checks | Prevents injection and runtime type errors |
| magic numbers | named constants | Intent is obvious and changes are centralized |
| deep inheritance trees | composition + factory functions | Flatter hierarchies reduce coupling |
| 1 | // Bad — var, ==, silent catch, magic number |
| 2 | function 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 |
| 10 | const API_BASE = "https://api.example.com"; |
| 11 | const TIMEOUT_MS = 5000; |
| 12 | |
| 13 | async 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
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.
| Convention | Recommendation |
|---|---|
| Variables | camelCase for variables/functions, PascalCase for classes, UPPER_SNAKE_CASE for constants |
| Declaration | Default to const; use let only when rebinding is required; never use var |
| Quotes | Pick single or double and stay consistent; use backticks for interpolation |
| Semicolons | Use them explicitly or rely on ASI consistently — do not mix |
| Line length | 80–100 characters; break long chains and parameter lists |
| Trailing commas | Always in multi-line objects/arrays/params for cleaner diffs |
| 1 | // Good style example |
| 2 | const MAX_RESULTS = 100; |
| 3 | |
| 4 | class 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 | |
| 23 | export { UserRepository }; |
info
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.
| 1 | // Destructuring with defaults |
| 2 | function 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 |
| 11 | const user = { id: 1, name: "Ada" }; |
| 12 | const updated = { ...user, name: "Ada Lovelace", lastLogin: new Date() }; |
| 13 | const { lastLogin, ...rest } = updated; |
| 14 | |
| 15 | // Optional chaining + nullish coalescing |
| 16 | const city = user?.address?.city ?? "Unknown"; |
| 17 | const count = response?.meta?.count ?? 0; // 0 is preserved, unlike || |
| 18 | |
| 19 | // Array helpers |
| 20 | const active = users.filter(u => u.isActive); |
| 21 | const names = active.map(u => u.name); |
| 22 | const total = cart.reduce((sum, item) => sum + item.price, 0); |
| 23 | |
| 24 | // Short-circuit for side-effect guards |
| 25 | isReady && startApp(); |
When transforming data, prefer immutable operations. They prevent hidden mutations and make React/Vue state updates predictable.
| 1 | // Bad — mutates the original array |
| 2 | function markDone(tasks) { |
| 3 | tasks.forEach(t => { t.done = true; }); |
| 4 | return tasks; |
| 5 | } |
| 6 | |
| 7 | // Good — returns a new array |
| 8 | function markAllDone(tasks) { |
| 9 | return tasks.map(t => ({ ...t, done: true })); |
| 10 | } |
| 11 | |
| 12 | // Even better — pure function with explicit predicate |
| 13 | function toggleTask(tasks, id) { |
| 14 | return tasks.map(t => |
| 15 | t.id === id ? { ...t, done: !t.done } : t |
| 16 | ); |
| 17 | } |
Async JavaScript is where most production bugs hide. Always await failures explicitly, avoid unhandled Promise rejections, and use AbortController to cancel in-flight requests.
| 1 | // Bad — unhandled rejection, no timeout |
| 2 | function getData() { |
| 3 | fetch("/api/data").then(res => res.json()); |
| 4 | } |
| 5 | |
| 6 | // Good — structured async with error boundaries |
| 7 | async 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 |
| 20 | async 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
| 1 | // Retry with exponential backoff |
| 2 | async 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 |
| 19 | const controller = new AbortController(); |
| 20 | fetchUser(1, controller.signal); |
| 21 | controller.abort(); // stops the fetch cleanly |
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.
| 1 | // utils/date.js — focused module with named exports |
| 2 | export function formatDate(date, locale = "en-US") { |
| 3 | return new Intl.DateTimeFormat(locale).format(date); |
| 4 | } |
| 5 | |
| 6 | export 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 |
| 13 | import { API_BASE } from "../config.js"; |
| 14 | |
| 15 | export 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 |
| 27 | export async function loadConfig() { /* ... */ } |
Organize modules by feature, not by layer. A feature folder groups components, utilities, and tests together, making ownership and refactoring easier.
| 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 |
| 10 | export 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 | |
| 16 | export 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 | } |
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__.
| 1 | my-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.
| 1 | // Fragile relative import |
| 2 | import { formatDate } from "../../../utils/date.js"; |
| 3 | |
| 4 | // Stable alias import |
| 5 | import { 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
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.
| 1 | // Debounce — wait for a pause in events |
| 2 | function debounce(fn, wait) { |
| 3 | let timer; |
| 4 | return function (...args) { |
| 5 | clearTimeout(timer); |
| 6 | timer = setTimeout(() => fn.apply(this, args), wait); |
| 7 | }; |
| 8 | } |
| 9 | window.addEventListener("resize", debounce(handleResize, 200)); |
| 10 | |
| 11 | // Throttle — run at most once per period |
| 12 | function 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 | } |
| 22 | window.addEventListener("scroll", throttle(handleScroll, 100)); |
| 23 | |
| 24 | // Efficient DOM batching |
| 25 | const fragment = document.createDocumentFragment(); |
| 26 | items.forEach(item => { |
| 27 | const li = document.createElement("li"); |
| 28 | li.textContent = item.name; |
| 29 | fragment.appendChild(li); |
| 30 | }); |
| 31 | list.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.
| 1 | // Cleanup pattern for long-lived listeners |
| 2 | let abortController; |
| 3 | |
| 4 | function subscribe(channel) { |
| 5 | abortController = new AbortController(); |
| 6 | eventBus.addEventListener(channel, handler, { signal: abortController.signal }); |
| 7 | } |
| 8 | |
| 9 | function unsubscribe() { |
| 10 | abortController?.abort(); // removes all listeners tied to this signal |
| 11 | } |
| 12 | |
| 13 | // Alternative: explicit removeEventListener |
| 14 | function setup() { |
| 15 | window.addEventListener("keydown", onKeyDown); |
| 16 | return () => window.removeEventListener("keydown", onKeyDown); |
| 17 | } |
info
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.
| 1 | // XSS-safe DOM insertion — never use innerHTML with user data |
| 2 | const userInput = "<img src=x onerror=alert('xss')>"; |
| 3 | |
| 4 | // Safe: textContent escapes HTML |
| 5 | const el = document.createElement("p"); |
| 6 | el.textContent = userInput; |
| 7 | container.appendChild(el); |
| 8 | |
| 9 | // Safe: use a sanitizer when HTML is required |
| 10 | import DOMPurify from "dompurify"; |
| 11 | container.innerHTML = DOMPurify.sanitize(userInput); |
| 12 | |
| 13 | // Avoid eval and new Function |
| 14 | // Bad |
| 15 | const result = eval(userCode); |
| 16 | |
| 17 | // Good — use a parser or structured data |
| 18 | const config = JSON.parse(userJson); |
| 19 | |
| 20 | // URL validation before redirecting |
| 21 | function 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.
| 1 | // Vite public env (safe to expose) |
| 2 | const apiUrl = import.meta.env.VITE_API_URL; |
| 3 | |
| 4 | // Node private env (server only) |
| 5 | const 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 |
| 12 | function validateEmail(value) { |
| 13 | if (typeof value !== "string" || value.length > 254) return false; |
| 14 | return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); |
| 15 | } |
danger
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.
| 1 | // Pure function unit test (Vitest/Jest syntax) |
| 2 | import { describe, it, expect } from "vitest"; |
| 3 | import { calculateTotals } from "./cartUtils.js"; |
| 4 | |
| 5 | describe("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 |
| 21 | import { vi } from "vitest"; |
| 22 | |
| 23 | globalThis.fetch = vi.fn(() => |
| 24 | Promise.resolve({ |
| 25 | ok: true, |
| 26 | json: () => Promise.resolve({ id: 1, name: "Ada" }), |
| 27 | }) |
| 28 | ); |
| 29 | |
| 30 | it("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.
| 1 | // Debugging — structured logs over console.log |
| 2 | function 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 |
| 13 | function complexLogic(state) { |
| 14 | if (state.invalid) debugger; // pauses in DevTools |
| 15 | return reconcile(state); |
| 16 | } |
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.
| 1 | // Focus management after modal opens |
| 2 | function openModal() { |
| 3 | modal.hidden = false; |
| 4 | previouslyFocused = document.activeElement; |
| 5 | closeButton.focus(); |
| 6 | } |
| 7 | |
| 8 | function closeModal() { |
| 9 | modal.hidden = true; |
| 10 | previouslyFocused?.focus(); |
| 11 | } |
| 12 | |
| 13 | // Keyboard trap inside a modal |
| 14 | trap.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 |
| 30 | const liveRegion = document.createElement("div"); |
| 31 | liveRegion.setAttribute("role", "status"); |
| 32 | liveRegion.setAttribute("aria-live", "polite"); |
| 33 | liveRegion.className = "sr-only"; |
| 34 | document.body.appendChild(liveRegion); |
| 35 | |
| 36 | function announce(message) { |
| 37 | liveRegion.textContent = message; |
| 38 | } |
best practice
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.
| 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.
| 1 | # .github/workflows/ci.yml |
| 2 | name: CI |
| 3 | on: [push, pull_request] |
| 4 | jobs: |
| 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
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.
| 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 | } |
| 1 | // Gradually add JSDoc types before full conversion |
| 2 | /** |
| 3 | * @param {string} id |
| 4 | * @returns {Promise<{ id: string; name: string }>} |
| 5 | */ |
| 6 | export 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 |
| 13 | export 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 | } |
A checklist keeps reviews consistent and thorough. Automate mechanical checks with CI so reviewers can focus on architecture, correctness, and edge cases.
| Check | Automated? | Details |
|---|---|---|
| Formatting | Prettier | Consistent quotes, spacing, and line breaks |
| Linting | ESLint | Unused vars, unsafe patterns, complexity rules |
| Types | TypeScript | Strict mode enabled, no implicit any |
| Tests | Vitest/Jest | New logic covered, existing tests pass |
| Error handling | Review | Async errors caught, user-facing messages safe |
| Security | ESLint / review | No eval, sanitized DOM insertion, validated inputs |
| Performance | Review | No unnecessary re-renders or network requests |
| Accessibility | axe / review | Keyboard support, focus management, ARIA labels |
| Naming | Review | Clear, intention-revealing names |
| Bundle size | size-limit / review | No heavy dependencies added without justification |
| 1 | # Run the full local CI pipeline before pushing |
| 2 | npm run format -- --check |
| 3 | npm run lint |
| 4 | npm run typecheck |
| 5 | npm run test:run |
| 6 | npm run build |
These references are maintained by the JavaScript community and browser vendors. Keep them bookmarked for authoritative answers and evolving standards.
- MDN JavaScript reference — comprehensive language and API docs
- ECMA-262 specification — the language standard
- ESLint documentation — configurable lint rules
- TypeScript handbook — typed JavaScript
- web.dev security — secure coding patterns