Mastering Modern JavaScript: Patterns You Actually Use

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

1const
by default; use
1let
only when you need to reassign. Avoid
1var
— it has function scope and hoisting behaviour that regularly causes bugs.

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

1this
— they close over the surrounding context's
1this
, which is almost always what you want inside class methods or React components.

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

1obj.x
access and pairs perfectly with function parameters.

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

1...
syntax serves double duty: spread copies/merges values; rest collects the remainder.

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
/
1await
is syntactic sugar over Promises. It makes asynchronous code read like synchronous code and keeps error handling in familiar
1try/catch
blocks.

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

1Promise.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.

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
/
1export
) are the standard way to split code across files, both in browsers and in Node.js (with
1"type": "module"
in
1package.json
or
1.mjs
extensions).

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.