Mastering Modern JavaScript: Patterns You Actually Use
JavaScript powers the web, but there's a big gap between knowing the basics and writing fluent modern JS. This post covers the patterns I reach for most often — all of them standard ES2015+ features available in every current browser and Node version.
Variables and Scope
Prefer
by default; use1const
only when you need to reassign. Avoid1let
— it has function scope and hoisting behaviour that regularly causes bugs.1var
1const API_URL = 'https://api.example.com'; // never reassigned 2 3let retries = 0; // will be incremented 4retries += 1; 5 6// var leaks out of blocks — this is why we don't use it 7for (var i = 0; i < 3; i++) {} 8console.log(i); // 3 — leaks! 9 10for (let j = 0; j < 3; j++) {} 11console.log(j); // ReferenceError — correctly scoped 12
Arrow Functions
Arrow functions are shorter and, crucially, they don't have their own
— they close over the surrounding context's1this
, which is almost always what you want inside class methods or React components.1this
1// traditional 2function double(n) { 3 return n * 2; 4} 5 6// arrow — same thing 7const double = (n) => n * 2; 8 9// multi-line body needs explicit return 10const clamp = (n, min, max) => { 11 if (n < min) return min; 12 if (n > max) return max; 13 return n; 14}; 15
Destructuring
Destructuring pulls values out of objects and arrays into named variables. It's cleaner than repeated
access and pairs perfectly with function parameters.1obj.x
1const user = { id: 1, name: 'Alice', role: 'admin' }; 2 3// object destructuring with a rename and a default 4const { name, role, department = 'Engineering' } = user; 5console.log(name); // 'Alice' 6console.log(department); // 'Engineering' 7 8// array destructuring 9const [first, second, ...rest] = [10, 20, 30, 40]; 10console.log(first); // 10 11console.log(rest); // [30, 40] 12 13// in function parameters — very common in React 14function UserCard({ name, role }) { 15 return `${name} (${role})`; 16} 17
Template Literals
Template literals replace string concatenation and support multi-line strings cleanly.
1const name = 'Alice'; 2const score = 42; 3 4// instead of: 'Hello, ' + name + '. Score: ' + score 5const message = `Hello, ${name}. Score: ${score}`; 6 7// multi-line — no \n needed 8const html = ` 9 <div class="card"> 10 <h2>${name}</h2> 11 <p>Score: ${score}</p> 12 </div> 13`.trim(); 14
Array Methods: map, filter, reduce
These three methods cover most list-processing needs without mutation or manual loops.
1const products = [ 2 { name: 'Widget', price: 9.99, inStock: true }, 3 { name: 'Gadget', price: 24.99, inStock: false }, 4 { name: 'Doohickey', price: 4.99, inStock: true }, 5]; 6 7// map — transform each element 8const names = products.map((p) => p.name); 9// ['Widget', 'Gadget', 'Doohickey'] 10 11// filter — keep elements that match a predicate 12const available = products.filter((p) => p.inStock); 13// [{name: 'Widget', ...}, {name: 'Doohickey', ...}] 14 15// reduce — collapse to a single value 16const totalStock = products 17 .filter((p) => p.inStock) 18 .reduce((sum, p) => sum + p.price, 0); 19// 14.98 20
Chain them together — each returns a new array, nothing is mutated.
Spread and Rest
The
syntax serves double duty: spread copies/merges values; rest collects the remainder.1...
1// spread: merge objects (last key wins) 2const defaults = { theme: 'light', language: 'en', debug: false }; 3const userPrefs = { theme: 'dark', notifications: true }; 4const config = { ...defaults, ...userPrefs }; 5// { theme: 'dark', language: 'en', debug: false, notifications: true } 6 7// spread: copy and extend arrays 8const base = [1, 2, 3]; 9const extended = [...base, 4, 5]; // [1, 2, 3, 4, 5] 10 11// rest in function params 12function sum(...numbers) { 13 return numbers.reduce((acc, n) => acc + n, 0); 14} 15sum(1, 2, 3, 4); // 10 16
Async / Await
/1async
is syntactic sugar over Promises. It makes asynchronous code read like synchronous code and keeps error handling in familiar1await
blocks.1try/catch
1async function fetchUser(id) { 2 try { 3 const res = await fetch(`https://api.example.com/users/${id}`); 4 if (!res.ok) throw new Error(`HTTP ${res.status}`); 5 const user = await res.json(); 6 return user; 7 } catch (err) { 8 console.error('Failed to load user:', err.message); 9 return null; 10 } 11} 12 13// parallel fetches — don't await sequentially when you don't need to 14async function loadDashboard(userId) { 15 const [user, posts] = await Promise.all([ 16 fetchUser(userId), 17 fetch(`/api/posts?author=${userId}`).then((r) => r.json()), 18 ]); 19 return { user, posts }; 20} 21
Awaiting two independent fetches sequentially doubles your latency. Use
when the requests don't depend on each other.1Promise.all
Optional Chaining and Nullish Coalescing
These two operators, added in ES2020, eliminate a huge category of defensive boilerplate.
1const data = { 2 user: { 3 profile: { 4 avatar: 'https://cdn.example.com/avatars/alice.png', 5 }, 6 }, 7}; 8 9// optional chaining: short-circuits to undefined instead of throwing 10const avatar = data?.user?.profile?.avatar; 11// 'https://cdn.example.com/avatars/alice.png' 12 13const missing = data?.user?.settings?.theme; 14// undefined — no TypeError 15 16// nullish coalescing: falls back only on null/undefined (not 0 or '') 17const theme = data?.user?.settings?.theme ?? 'light'; 18// 'light' 19 20// compare with ||, which also catches falsy values like 0 and '' 21const count = 0; 22console.log(count || 10); // 10 — probably wrong 23console.log(count ?? 10); // 0 — correct 24
Modules
ES modules (
/1import
) are the standard way to split code across files, both in browsers and in Node.js (with1export
in1"type": "module"
or1package.json
extensions).1.mjs
1// math.js 2export function add(a, b) { return a + b; } 3export function subtract(a, b) { return a - b; } 4export const PI = 3.14159; 5 6// default export — one per file 7export default function multiply(a, b) { return a * b; } 8 9// main.js 10import multiply, { add, PI } from './math.js'; 11// or rename on import 12import { add as sum } from './math.js'; 13 14console.log(sum(2, 3)); // 5 15console.log(multiply(4, PI)); // ~12.57 16
These aren't exotic features — they're the everyday vocabulary of modern JavaScript. Getting comfortable with destructuring, array methods, and async/await will make you faster to read and write idiomatic React, Node, and Next.js code, since those ecosystems lean on all of them heavily.
