Modern JavaScript: Beyond the Basics
Modern JavaScript features that go beyond tutorials, the patterns that separate intermediate from advanced developers.
Modern JavaScript: Beyond the Basics
JavaScript has changed a lot in the last few years. Here are the patterns I reach for most, past the basics everyone already knows.
Advanced async patterns
Promise Combinators
Beyond Promise.all, modern JavaScript has several other combinators:
// Promise.allSettled - Wait for all, regardless of success/failure
const results = await Promise.allSettled([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments'),
]);
// Promise.race - First to finish wins
const fastest = await Promise.race([
fetch('/api/server1'),
fetch('/api/server2'),
]);
// Promise.any - First success wins (ignores failures)
const firstSuccess = await Promise.any([
fetchFromCDN1(),
fetchFromCDN2(),
fetchFromCDN3(),
]);
Async iteration
async function* generateData() {
for (let i = 0; i < 5; i++) {
await delay(1000);
yield i;
}
}
// Consume with for-await-of
for await (const value of generateData()) {
console.log(value); // 0, 1, 2, 3, 4 (with 1s delays)
}
Advanced object patterns
Proxy for validation
const createValidatedObject = (schema) => {
return new Proxy(
{},
{
set(target, property, value) {
const validator = schema[property];
if (!validator || validator(value)) {
target[property] = value;
return true;
}
throw new Error(`Invalid value for ${property}`);
},
}
);
};
const user = createValidatedObject({
age: (val) => typeof val === 'number' && val > 0,
email: (val) => /\S+@\S+\.\S+/.test(val),
});
user.age = 25; // OK
user.email = 'test@example.com'; // OK
user.age = -5; // Throws error
Private fields
class BankAccount {
#balance = 0; // Private field
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
console.log(account.#balance); // SyntaxError
Functional patterns
Composition and pipe
const compose =
(...fns) =>
(x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const pipe =
(...fns) =>
(x) =>
fns.reduce((acc, fn) => fn(acc), x);
// Usage
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;
const calculate = pipe(
addOne, // 6
double, // 12
square // 144
);
console.log(calculate(5)); // 144
Currying
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
};
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
Performance optimization
Memoization
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
};
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
WeakMap for private data
const privateData = new WeakMap();
class User {
constructor(name, password) {
this.name = name;
privateData.set(this, { password });
}
verifyPassword(input) {
return privateData.get(this).password === input;
}
}
Modern tooling integration
Optional chaining and nullish coalescing
// Optional chaining
const userName = user?.profile?.name ?? 'Guest';
// Nullish coalescing (only null/undefined, not falsy)
const count = response.count ?? 0; // 0 if null/undefined
const isActive = response.isActive ?? true; // true if null/undefined
Top-level await (ES2022)
// In modules, you can use await at the top level
const data = await fetch('/api/config').then((r) => r.json());
export default data;
Conclusion
Most of these patterns solve a specific problem: Proxy catches invalid state before it corrupts your app, WeakMap keeps private data from leaking into a serialized object, memoization skips redundant work. None of them belong in your code by default.
I’ve seen more bugs introduced by someone reaching for currying because it looked clever than by someone writing a boring for loop. Use the advanced tool when the boring one actually fails you, not before.