Introduction: JavaScript in 2025
JavaScript continues to evolve at a remarkable pace. With ES2024 and ES2025 features landing in browsers and runtimes, the language is more powerful than ever. But power without discipline leads to unmaintainable code. This guide covers the best practices, patterns, and techniques that elite JavaScript developers use in 2025 to write clean, performant, and secure code.
Whether you're building a complex React application, a Node.js backend, or a serverless function, these practices will make your code more readable, maintainable, and bug-resistant.
ES2024/2025 Features You Should Be Using
The Temporal API: Finally, Proper Dates
The Temporal API is the long-awaited replacement for the notoriously broken Date object. It provides immutable date/time objects, proper time zone support, and intuitive APIs.
// Creating dates with Temporal (no more month-indexing confusion!)
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // "2025-05-31"
// Create a specific date
const launch = Temporal.PlainDate.from({ year: 2025, month: 7, day: 15 });
console.log(launch.toString()); // "2025-07-15"
// Date arithmetic is intuitive and immutable
const nextWeek = today.add({ days: 7 });
const threeMonthsLater = today.add({ months: 3 });
console.log(nextWeek.toString()); // "2025-06-07"
// Duration between dates
const duration = today.until(launch);
console.log(duration.toString()); // "P45D" (45 days)
console.log(duration.days); // 45
// Time zones done right
const meeting = Temporal.ZonedDateTime.from({
year: 2025,
month: 6,
day: 15,
hour: 14,
minute: 0,
timeZone: "America/New_York"
});
// Convert to another timezone
const tokyoTime = meeting.withTimeZone("Asia/Tokyo");
console.log(tokyoTime.toString());
// "2025-06-16T03:00:00+09:00[Asia/Tokyo]"
// Formatting
const formatted = meeting.toLocaleString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
timeZoneName: "short"
});
// "Sunday, June 15, 2025, 02:00 PM EDT"
Records and Tuples: Immutable Data Structures
Records and Tuples are deeply immutable versions of objects and arrays. They use value equality instead of reference equality.
// Records (immutable objects) — use #{ } syntax
const user = #{
name: "Alice",
age: 30,
role: "engineer"
};
// Tuples (immutable arrays) — use #[ ] syntax
const coordinates = #[40.7128, -74.0060];
// Value equality (unlike regular objects)
const a = #{ x: 1, y: 2 };
const b = #{ x: 1, y: 2 };
console.log(a === b); // true (same values = equal!)
// With regular objects:
const c = { x: 1, y: 2 };
const d = { x: 1, y: 2 };
console.log(c === d); // false (different references)
// Records can be used as Map keys
const cache = new Map();
cache.set(#{ endpoint: "/api/users", page: 1 }, userData);
// Spreading creates new records/tuples
const updated = #{ ...user, age: 31 };
console.log(user.age); // 30 (unchanged)
console.log(updated.age); // 31
Pattern Matching (Stage 3 Proposal)
// Pattern matching replaces complex if/else and switch statements
const result = match (response) {
when ({ status: 200, body }) -> processData(body),
when ({ status: 201, headers }) -> handleCreated(headers),
when ({ status: 404 }) -> handleNotFound(),
when ({ status: >= 400 and < 500 }) -> handleClientError(response),
when ({ status: >= 500 }) -> handleServerError(response),
default -> handleUnknown(response)
};
// Matching with destructuring and guards
const describe = (value) => match (value) {
when (0) -> "zero",
when (n) if (n > 0) -> "positive",
when (n) if (n < 0) -> "negative",
when (NaN) -> "not a number"
};
Decorators (Stage 3)
// Decorators add behavior to classes and methods
function logged(target, context) {
return function (...args) {
console.log(`Calling ${context.name} with`, args);
const result = target.apply(this, args);
console.log(`${context.name} returned`, result);
return result;
};
}
function memoize(target, context) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = target.apply(this, args);
cache.set(key, result);
return result;
};
}
class MathService {
@logged
@memoize
fibonacci(n) {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const math = new MathService();
math.fibonacci(40); // Logged and cached
Async/Await Patterns: Beyond the Basics
Promise.all: Parallel Execution
// Run multiple async operations concurrently
// FAILS FAST: If any promise rejects, the entire call rejects
async function fetchDashboardData(userId) {
try {
const [user, orders, notifications] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchNotifications(userId)
]);
return { user, orders, notifications };
} catch (error) {
// Any single failure causes this catch block
console.error("Dashboard fetch failed:", error);
throw error;
}
}
// With timeout protection
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
const data = await withTimeout(
Promise.all([fetchUser(1), fetchOrders(1)]),
5000 // 5 second timeout
);
Promise.allSettled: Handle Mixed Results
// Unlike Promise.all, this NEVER rejects
// It waits for ALL promises to settle (resolve or reject)
async function fetchAllUserData(userIds) {
const results = await Promise.allSettled(
userIds.map(id => fetchUser(id))
);
const successful = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
const failed = results
.filter(r => r.status === "rejected")
.map(r => r.reason);
console.log(`Fetched ${successful.length}/${userIds.length} users`);
if (failed.length > 0) {
console.warn(`${failed.length} fetches failed:`, failed);
}
return { successful, failed };
}
Promise.race and Promise.any
// Promise.race: First to settle (resolve OR reject) wins
const fastest = await Promise.race([
fetch("https://api-us.example.com/data"),
fetch("https://api-eu.example.com/data"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 3000)
)
]);
// Promise.any: First to RESOLVE wins (ignores rejections)
// Only rejects if ALL promises reject
const result = await Promise.any([
fetch("https://primary-api.example.com/data"),
fetch("https://backup-api.example.com/data"),
fetch("https://cache.example.com/data")
]);
for-await-of: Async Iteration
// Process streams of async data
async function* fetchPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data.items;
hasMore = data.hasNextPage;
page++;
}
}
// Consume the async generator
async function getAllItems() {
const allItems = [];
for await (const items of fetchPages("https://api.example.com/products")) {
allItems.push(...items);
console.log(`Loaded ${allItems.length} items so far...`);
}
return allItems;
}
Error Handling Strategies
Custom Error Classes
// Create a hierarchy of custom errors
class AppError extends Error {
constructor(message, statusCode, errorCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.errorCode = errorCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, fields = []) {
super(message, 400, "VALIDATION_ERROR");
this.fields = fields;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 404, "NOT_FOUND");
this.resource = resource;
this.resourceId = id;
}
}
class AuthenticationError extends AppError {
constructor(message = "Authentication required") {
super(message, 401, "AUTH_REQUIRED");
}
}
// Usage
async function getUser(id) {
const user = await db.users.findById(id);
if (!user) throw new NotFoundError("User", id);
return user;
}
// Global error handler (Express example)
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: {
code: err.errorCode,
message: err.message,
...(err.fields && { fields: err.fields })
}
});
}
// Unexpected errors
console.error("Unhandled error:", err);
res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "Something went wrong" } });
});
Global Unhandled Error Handlers
// Browser
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
// Send to error tracking service
errorTracker.captureException(event.reason);
event.preventDefault();
});
window.addEventListener("error", (event) => {
console.error("Global error:", event.error);
errorTracker.captureException(event.error);
});
// Node.js
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
errorTracker.captureException(reason);
});
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
errorTracker.captureException(error);
process.exit(1); // Exit on uncaught exceptions
});
Module System: ESM vs CJS
// CommonJS (CJS) — the old way, still used in many Node.js projects
const express = require("express");
const { readFile } = require("fs/promises");
module.exports = { myFunction };
module.exports = MyClass;
// ES Modules (ESM) — the standard, use this for new projects
import express from "express";
import { readFile } from "fs/promises";
export function myFunction() { /* ... */ }
export default class MyClass { /* ... */ }
// Dynamic imports (lazy loading / code splitting)
const module = await import("./heavy-module.js");
// Conditional imports
let db;
if (process.env.DB_TYPE === "postgres") {
db = await import("./postgres-driver.js");
} else {
db = await import("./sqlite-driver.js");
}
To enable ESM in Node.js, add "type": "module" to your package.json:
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"engines": {
"node": ">=20.0.0"
}
}
Closures and Scope: Explained with Real Examples
// Closures: A function that remembers variables from its outer scope
function createCounter(initialValue = 0) {
let count = initialValue; // This variable is "closed over"
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; },
reset() { count = initialValue; return count; }
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.getCount()); // 12
// count variable is private — cannot be accessed directly
// Practical use: Creating private state
function createRateLimiter(maxRequests, windowMs) {
const requests = new Map();
return function isAllowed(clientId) {
const now = Date.now();
const windowStart = now - windowMs;
// Clean old entries
const clientRequests = (requests.get(clientId) || [])
.filter(timestamp => timestamp > windowStart);
if (clientRequests.length >= maxRequests) {
return false;
}
clientRequests.push(now);
requests.set(clientId, clientRequests);
return true;
};
}
const limiter = createRateLimiter(100, 60000); // 100 requests per minute
app.use((req, res, next) => {
if (!limiter(req.ip)) {
return res.status(429).json({ error: "Too many requests" });
}
next();
});
Functional Programming Patterns
Map, Filter, Reduce: The Holy Trinity
const orders = [
{ id: 1, product: "Laptop", price: 999, quantity: 1, status: "completed" },
{ id: 2, product: "Mouse", price: 29, quantity: 3, status: "completed" },
{ id: 3, product: "Keyboard", price: 79, quantity: 1, status: "pending" },
{ id: 4, product: "Monitor", price: 449, quantity: 2, status: "completed" },
{ id: 5, product: "USB Cable", price: 12, quantity: 5, status: "cancelled" }
];
// Pipeline: filter → map → reduce
const totalCompletedRevenue = orders
.filter(order => order.status === "completed")
.map(order => order.price * order.quantity)
.reduce((total, amount) => total + amount, 0);
console.log(totalCompletedRevenue); // 1984
Currying and Composition
// Currying: Transform f(a, b, c) into f(a)(b)(c)
const curry = (fn) => {
const arity = fn.length;
return function curried(...args) {
if (args.length >= arity) return fn(...args);
return (...moreArgs) => curried(...args, ...moreArgs);
};
};
const add = curry((a, b) => a + b);
const multiply = curry((a, b) => a * b);
const add10 = add(10);
const double = multiply(2);
console.log(add10(5)); // 15
console.log(double(7)); // 14
// Function composition: pipe and compose
const pipe = (...fns) => (value) =>
fns.reduce((acc, fn) => fn(acc), value);
const compose = (...fns) => (value) =>
fns.reduceRight((acc, fn) => fn(acc), value);
// Build a data processing pipeline
const processUser = pipe(
(user) => ({ ...user, name: user.name.trim() }),
(user) => ({ ...user, email: user.email.toLowerCase() }),
(user) => ({ ...user, age: parseInt(user.age, 10) }),
(user) => ({ ...user, isAdult: user.age >= 18 })
);
const user = processUser({
name: " Alice ",
email: "ALICE@Example.COM",
age: "25"
});
// { name: "Alice", email: "alice@example.com", age: 25, isAdult: true }
Web Workers for Heavy Computation
// main.js - Create and communicate with a Web Worker
const worker = new Worker("./worker.js");
worker.postMessage({
type: "PROCESS_DATA",
payload: largeDataSet
});
worker.addEventListener("message", (event) => {
const { type, result } = event.data;
if (type === "PROCESS_COMPLETE") {
console.log("Processing done:", result);
updateUI(result);
}
});
worker.addEventListener("error", (error) => {
console.error("Worker error:", error.message);
});
// worker.js - Runs in a separate thread
self.addEventListener("message", (event) => {
const { type, payload } = event.data;
if (type === "PROCESS_DATA") {
// Heavy computation happens here without blocking the UI
const result = processLargeDataSet(payload);
self.postMessage({ type: "PROCESS_COMPLETE", result });
}
});
function processLargeDataSet(data) {
// CPU-intensive work: sorting, filtering, aggregating
return data
.filter(item => item.isValid)
.map(item => transform(item))
.sort((a, b) => b.score - a.score);
}
Testing with Vitest
// math.test.js
import { describe, it, expect, vi, beforeEach } from "vitest";
import { calculateDiscount, fetchUserData } from "./services.js";
describe("calculateDiscount", () => {
it("applies 10% discount for orders over $100", () => {
expect(calculateDiscount(150)).toBe(135);
});
it("applies no discount for orders under $100", () => {
expect(calculateDiscount(50)).toBe(50);
});
it("throws for negative amounts", () => {
expect(() => calculateDiscount(-10)).toThrow("Amount must be positive");
});
});
// Mocking API calls
describe("fetchUserData", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("returns user data on success", async () => {
const mockUser = { id: 1, name: "Alice" };
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUser)
});
const user = await fetchUserData(1);
expect(user).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith("/api/users/1");
});
it("throws NotFoundError on 404", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404
});
await expect(fetchUserData(999)).rejects.toThrow("User not found");
});
});
Security: Preventing XSS and Common Attacks
// 1. NEVER use innerHTML with user input
// BAD
element.innerHTML = userInput;
// GOOD
element.textContent = userInput;
// 2. Sanitize HTML when you must render it
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
ALLOWED_ATTR: ["href", "target"]
});
// 3. Content Security Policy headers (Express)
import helmet from "helmet";
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
}
}));
// 4. Validate and sanitize all inputs
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email(),
age: z.number().int().min(13).max(120)
});
app.post("/api/users", (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data is validated and typed
createUser(result.data);
});
Design Patterns in Modern JavaScript
Observer Pattern (Event Emitter)
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(callback);
return () => this.off(event, callback); // Return unsubscribe function
}
off(event, callback) {
this.#listeners.get(event)?.delete(callback);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(cb => cb(...args));
}
once(event, callback) {
const unsubscribe = this.on(event, (...args) => {
callback(...args);
unsubscribe();
});
}
}
// Usage
const store = new EventEmitter();
const unsubscribe = store.on("userUpdated", (user) => {
console.log("User updated:", user.name);
});
store.emit("userUpdated", { name: "Alice" });
unsubscribe(); // Clean up
Factory Pattern
class DatabaseFactory {
static create(type, config) {
switch (type) {
case "postgres":
return new PostgresDatabase(config);
case "mongodb":
return new MongoDatabase(config);
case "sqlite":
return new SQLiteDatabase(config);
default:
throw new Error(`Unknown database type: ${type}`);
}
}
}
// Usage
const db = DatabaseFactory.create(process.env.DB_TYPE, {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
name: process.env.DB_NAME
});
Quick Reference: JavaScript Best Practices Cheat Sheet
| Category | Do This | Avoid This |
|---|---|---|
| Variables | const by default, let when needed | var (function-scoped, hoisted) |
| Equality | === strict equality | == loose equality (type coercion) |
| Strings | Template literals: `Hello ${name}` | String concatenation: "Hello " + name |
| Loops | for...of for arrays, for...in for objects | for (var i=0; ...) in most cases |
| Functions | Arrow functions for callbacks | Anonymous function expressions |
| Objects | Destructuring: const { a, b } = obj | Repeated obj.a, obj.b |
| Async | async/await with try/catch | Nested .then().then().catch() |
| Errors | Custom error classes | Throwing plain strings |
| Modules | ESM: import/export | CJS in new projects |
| Security | textContent, DOMPurify | innerHTML with user data |
| Types | TypeScript or JSDoc type annotations | Untyped code in large projects |
| Testing | Vitest or Jest with good coverage | No tests or manual testing only |