JavaScript Best Practices
You’ve covered the language end to end. This final chapter is a field guide — the conventions, patterns, and trade-offs that turn working JavaScript into maintainable JavaScript. Treat it as a checklist you can return to before you ship.
1. Variables & Types
- Default to
const. Useletonly when you genuinely need to reassign. Never usevar. - Prefer specific names (
activeUsers) over generic ones (data,info,temp). - Beware implicit coercion. Use
===/!==, and explicit conversions:Number(x),String(x),Boolean(x). - Use
??for nullish defaults,||only when falsy (0,"") should also trigger the fallback.
const port = config.port ?? 3000; // ✅ keeps 0
const verbose = config.verbose || false; // ❌ "" or 0 → false
- Use template literals for any non-trivial string assembly — never
+.
2. Functions
- Keep them small (one screen, one job). Long functions are the #1 source of bugs.
- Pure where possible — same input → same output, no side effects. Pure functions test trivially.
- Default parameters beat
||fallbacks. - Avoid mutable defaults in shared closures (
function (arr = []) {}is fine — fresh array each call). - Arrow functions for callbacks; regular functions or methods when you need
thisor hoisting. - No
argumentsin modern code — use(...args)rest.
// ✅
function send({ to, subject, body, retries = 3 } = {}) { /* … */ }
// ❌
function send(to, subject, body, retries) {
retries = retries || 3;
/* … */
}
3. Async Code
- Always
await— never leave a promise floating without handling rejection. - Use
try/catcharoundawait, or a.catchon the chain. - Parallelise independent calls with
Promise.all(orallSettledfor partial success). - Sequence dependent calls intentionally — sequential
awaits on independent work is a silent slowdown. - Always add a timeout to outbound HTTP via
AbortSignal.timeout(ms). - Never use
asyncinsideforEach—forEachignores the returned promise. Usefor…oforPromise.all(map(...)).
// ❌ silently sequential, ignores errors
items.forEach(async (i) => await save(i));
// ✅ parallel and observed
await Promise.all(items.map(save));
4. Equality, Truthiness, and Numbers
===/!==for everything — including comparing tonull(yes,x == nullis the one legitimate use of==, butx === null || x === undefinedis even clearer).- Remember the falsy values:
false,0,"",null,undefined,NaN,0n. - For floats:
Math.abs(a - b) < 1e-9instead ofa === b. - For very large integers: use
BigInt(123n). - Use
Number.isFinite(x)andNumber.isInteger(x)— never the globalisNaN/isFinite(they coerce).
5. Arrays and Objects
- Use spread
{...obj}/[...arr]instead ofObject.assign/concatfor cloning. - Use destructuring for named-arg style and for unpacking responses.
- Immutable updates win in shared state: produce new arrays/objects instead of mutating in place (
map,filter,{ ...obj, key: v }). Object.freezeyour constants so accidents become errors.- For dictionaries with unknown keys, prefer
Mapover plain objects (no prototype pollution).
// ✅
const next = { ...prev, name: "Ada" };
// ❌
prev.name = "Ada"; // mutates shared state
6. Modules
- Use ESM (
import/export). Avoid mixing with CommonJS in the same package. - Prefer named exports over defaults — better refactor, grep, and tree-shake.
- Keep modules small and focused — one concept per file.
- Use
import()for code-splitting on-demand features. - Watch out for barrel files that hurt tree-shaking; deep-import in hot paths.
7. Classes vs Functions
- Use a class when you have state + behaviour bundled.
- Use plain functions + closures for everything else.
- Use
#privatefields — never_privateconventions — for true encapsulation. - Prefer composition over inheritance. Keep
extendschains short. - Use getters/setters to wrap validation around access.
8. Error Handling
- Always throw
new Error(...)(or a subclass). Never strings. - Define typed error classes for branching reactions (
ValidationError,NotFoundError). - Catch at the boundary where you can do something useful — not in every helper.
- Re-throw with
{ cause }to add context without losing the original. - Register global handlers (
unhandledrejection,error) for reporting — not for recovery. - Don’t return errors as values (Go-style) in JavaScript — use exceptions; that’s what the language is built around.
9. The DOM & Events
- Cache DOM lookups; don’t
querySelectorin a loop. - Use event delegation for many child elements.
- Always remove listeners when components unmount (
AbortControlleris ideal). - Prefer
datasetover arbitrary attributes for component-meta. - Batch reads/writes to avoid layout thrash (read all → write all).
const ctrl = new AbortController();
btn.addEventListener("click", onClick, { signal: ctrl.signal });
// later
ctrl.abort();
10. Performance
- Measure first. Profile with DevTools /
performance.now()— don’t optimise from intuition. - Avoid the classic O(n²) —
array.includesinside a loop becomesSet.has. - Debounce input handlers, throttle scroll/resize.
- Lazy-load heavy modules with dynamic
import(). - Stream large data via async generators instead of buffering arrays.
- Avoid layout thrash in DOM code (group reads then writes).
11. Security
- Never
innerHTMLuser content. UsetextContentor proper escaping. - Avoid
evaland theFunctionconstructor on dynamic input. - Use
crypto.randomUUID()/crypto.getRandomValues— neverMath.random()for tokens or IDs. - Validate at the boundary — never trust input from URL, form, network, or storage.
- Escape user-supplied strings before regex via a helper.
- Set sensible HTTP headers: CSP, X-Content-Type-Options, etc. (server-side concern).
- Treat dependencies as attack surface — audit, pin, and limit.
12. Code Style & Tooling
| Tool | Job |
|---|---|
| Prettier | Auto-format. Stop arguing about commas. |
| ESLint | Lint, catch bugs, enforce conventions. |
| TypeScript (or JSDoc types) | Static safety, better IDE. |
| Vitest / Jest | Unit + integration tests. |
| Playwright / Cypress | End-to-end browser tests. |
npm audit / Snyk / Dependabot |
Catch vulnerable deps. |
Settle these once at the project root and stop re-deciding per PR.
13. Testing
- Test behaviour, not implementation. “What does the user / caller see?” — not “did this method get called?”
- One assertion per test (most of the time). Failures tell you the cause immediately.
- Arrange / Act / Assert structure.
- Use real timers and real promises when you can — mock the boundary, not your own code.
- Cover the error paths — many bugs hide in catch blocks no one ever ran.
14. Naming and Comments
- Code is read far more than it is written. Names matter more than cleverness.
- Use verbs for functions (
fetchUser), nouns for data (user,users), adjectives/is/hasfor booleans (isReady,hasError). - Comment the why, not the what.
- Delete commented-out code — git remembers.
15. A “First Hour on a New Project” Checklist
- Node version pinned (
.nvmrc/engines) - ESM enabled (
"type": "module") - Prettier + ESLint configured and running in CI
- Tests with one CI command (
npm test) - Strict mode everywhere (modules are by default)
- No
var, no==, no implicit globals - All external calls have a timeout
- All
awaited promises have atry/catchor a chain.catch - No
innerHTMLwith user content - No
Math.random()for security-sensitive values - Errors are real
Errorinstances with useful messages - No deeply nested
extends - No barrel files hurting tree-shaking in hot paths
- No floating promises (linter rule on)
16. Recurring Mental Models
- “Everything in JavaScript is either a primitive or an object.”
- “A function is a value.” First-class functions enable callbacks, HOFs, closures, composition.
- “Reference equality, not value equality.”
{a:1} === {a:1}isfalse. - “The event loop is single-threaded.” Long sync work blocks the UI. Break it up or move to a Worker.
- “Promises are values.” They represent eventual completion — store them, pass them, await them.
- “Modules are live.” Imports see the latest value, not a snapshot.
17. When in Doubt
- Choose the boring option. Boring code is the easiest to change later.
- Reach for standard library (
URL,URLSearchParams,Intl,crypto,structuredClone,AbortController) before adding a dependency. - Optimise clarity first, performance when measured, cleverness never.
What’s Next?
That’s the JavaScript tutorial — from your first console.log to streaming async generators and production-grade defaults. Bookmark this chapter; revisit it on your next code review.
Where to go from here:
- Frameworks — apply these patterns to React, Vue, Svelte, or Angular.
- Node.js — write CLIs, servers, and tools.
- TypeScript — add a type layer that catches whole categories of bugs at edit time.
- Web Platform deep-dives — Web Components, Service Workers, WebRTC, WebGPU, Workers.
- Testing — Vitest / Jest / Playwright to lock the behaviour in.
- Performance — DevTools Performance panel, Lighthouse, Web Vitals.
Keep building. The best way to grow as a JS developer is to ship — then read your own code three months later and rewrite the parts that hurt.