Modern JavaScript Patterns Worth Knowing

Updated 2026-03-04

The JavaScript ecosystem moves fast, but certain patterns stabilize and become idioms. These are the ones that have earned their place in production codebases — not because they’re clever, but because they make intent clear and eliminate whole categories of bugs.

Nullish Coalescing and Optional Chaining

These two operators work as a pair and cover the majority of defensive programming needs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const config = {
theme: {
colors: {
accent: '#89b4fa',
},
},
};

// Optional chaining — short-circuits to undefined instead of throwing
const accent = config?.theme?.colors?.accent;
const missing = config?.sidebar?.width; // undefined, not an error

// Nullish coalescing — only falls back on null/undefined (not 0 or '')
const width = config?.sidebar?.width ?? 240;
const label = config?.nav?.label ?? 'Home';

// Combine for safe method calls
const items = config?.nav?.getItems?.() ?? [];

The key distinction: ?? differs from || in that 0, '', and false are valid values that || would skip over.


Object Patterns

Destructuring with Defaults

1
2
3
4
5
6
7
8
9
10
11
12
function createPost({
title = 'Untitled',
date = new Date(),
tags = [],
draft = false,
excerpt = '',
} = {}) {
return { title, date, tags, draft, excerpt };
}

// Caller only specifies what they need
const post = createPost({ title: 'My Post', tags: ['js'] });

Pick and Omit via Destructuring

1
2
3
4
5
6
7
8
9
// Omit: exclude `password` from the returned object
function sanitizeUser({ password, ...safeUser }) {
return safeUser;
}

// Pick: take only what you need
function getDisplayName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}

Merging with Spread

1
2
3
4
5
6
const defaults = { theme: 'dark', lang: 'en', perPage: 10 };
const userPrefs = { theme: 'light', perPage: 20 };

// Later values win — mirrors Object.assign
const resolved = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'en', perPage: 20 }

Array Patterns

Grouping with Object.groupBy (ES2024)

1
2
3
4
5
6
7
8
9
10
11
const posts = [
{ title: 'CSS Guide', category: 'Development' },
{ title: 'JS Patterns', category: 'Development' },
{ title: 'Note-taking', category: 'Productivity' },
];

const byCategory = Object.groupBy(posts, post => post.category);
// {
// Development: [ { title: 'CSS Guide' }, { title: 'JS Patterns' } ],
// Productivity: [ { title: 'Note-taking' } ]
// }

Immutable Updates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const state = { posts: ['a', 'b', 'c'], page: 1 };

// Add item
const withNew = { ...state, posts: [...state.posts, 'd'] };

// Remove item by index
const withoutFirst = {
...state,
posts: state.posts.filter((_, i) => i !== 0),
};

// Update item by index
const updated = {
...state,
posts: state.posts.map((p, i) => i === 1 ? 'B' : p),
};

Async Patterns

Parallel vs Sequential

1
2
3
4
5
6
7
8
9
10
11
// SEQUENTIAL — each waits for the previous (slow)
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(user.id);

// PARALLEL — all fire at once (fast when independent)
const [user, posts, comments] = await Promise.all([
getUser(id),
getPosts(id),
getComments(id),
]);

Error Handling Without try/catch Everywhere

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Wrap in a utility that returns [error, result]
async function attempt(promise) {
try {
return [null, await promise];
} catch (err) {
return [err, null];
}
}

// Clean call sites
const [err, data] = await attempt(fetchPosts());
if (err) {
console.error('Failed to fetch posts:', err.message);
return;
}
renderPosts(data);

Timeout Wrapper

1
2
3
4
5
6
7
8
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}

const data = await withTimeout(fetch('/api/posts'), 5000);

The Module Pattern (ESM)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// posts.js — explicit public API via named exports
const POSTS_PER_PAGE = 10;

function paginate(posts, page = 1) {
const start = (page - 1) * POSTS_PER_PAGE;
return posts.slice(start, start + POSTS_PER_PAGE);
}

function sortByDate(posts, direction = 'desc') {
return [...posts].sort((a, b) => {
const diff = new Date(a.date) - new Date(b.date);
return direction === 'desc' ? -diff : diff;
});
}

export { paginate, sortByDate };
export const DEFAULT_PAGE_SIZE = POSTS_PER_PAGE;

Avoiding Common Pitfalls

Closure in Loops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Bug: all callbacks capture the same `i` (var)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // prints 3, 3, 3
}

// Fix 1: use let (block-scoped, new binding each iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // prints 0, 1, 2
}

// Fix 2: use forEach (each callback gets its own scope)
[0, 1, 2].forEach(i => {
setTimeout(() => console.log(i), 100); // prints 0, 1, 2
});

Object Reference Traps

1
2
3
4
5
6
7
8
9
10
11
12
// Both variables point to the same array
const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4] — mutated!

// Shallow copy
const c = [...a];
const d = Object.assign({}, { x: 1 });

// Deep copy (ES2022+)
const e = structuredClone(a);

When Not to Use a Pattern

Patterns have costs — cognitive overhead, indirection, and abstraction layers. The right question is not “can I use this pattern here?” but “does this pattern solve a real problem I have right now?”

Write the simplest code that works. Reach for patterns when the simple version breaks down — when you’re repeating yourself, when you’re writing defensive code against undefined in three places, or when a bug points to a structural issue.