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