Modern JavaScript: ES2024 Features and Advanced Patterns
Alex Thompson

Explore the latest JavaScript features, advanced patterns, and best practices for building modern web applications with ES2024 and beyond.
JavaScript continues to evolve rapidly, with ES2024 bringing exciting new features that make the language more powerful and expressive. This guide explores the latest JavaScript features and advanced patterns that will transform how you write modern applications.
ES2024 New Features
Array Grouping Methods
ES2024 introduces native array grouping methods that eliminate the need for external libraries.
// Array.prototype.group() - Group by a key function
const products = [
{ name: 'Laptop', category: 'Electronics', price: 999 },
{ name: 'Shirt', category: 'Clothing', price: 29 },
{ name: 'Phone', category: 'Electronics', price: 699 },
{ name: 'Jeans', category: 'Clothing', price: 79 },
{ name: 'Tablet', category: 'Electronics', price: 399 }
];
// Group by category
const groupedByCategory = products.group(product => product.category);
console.log(groupedByCategory);
// {
// Electronics: [
// { name: 'Laptop', category: 'Electronics', price: 999 },
// { name: 'Phone', category: 'Electronics', price: 699 },
// { name: 'Tablet', category: 'Electronics', price: 399 }
// ],
// Clothing: [
// { name: 'Shirt', category: 'Clothing', price: 29 },
// { name: 'Jeans', category: 'Clothing', price: 79 }
// ]
// }
// Array.prototype.groupToMap() - Group to a Map
const groupedByPriceRange = products.groupToMap(product => {
if (product.price < 100) return 'Budget';
if (product.price < 500) return 'Mid-range';
return 'Premium';
});
console.log(groupedByPriceRange);
// Map {
// 'Premium' => [{ name: 'Laptop', ... }, { name: 'Phone', ... }],
// 'Budget' => [{ name: 'Shirt', ... }, { name: 'Jeans', ... }],
// 'Mid-range' => [{ name: 'Tablet', ... }]
// }
// Advanced grouping with multiple criteria
const users = [
{ name: 'Alice', age: 25, department: 'Engineering', level: 'Senior' },
{ name: 'Bob', age: 30, department: 'Engineering', level: 'Junior' },
{ name: 'Carol', age: 28, department: 'Marketing', level: 'Senior' },
{ name: 'David', age: 35, department: 'Marketing', level: 'Senior' }
];
const groupedUsers = users.group(user => `${user.department}-${user.level}`);
console.log(groupedUsers);
// {
// 'Engineering-Senior': [{ name: 'Alice', ... }],
// 'Engineering-Junior': [{ name: 'Bob', ... }],
// 'Marketing-Senior': [{ name: 'Carol', ... }, { name: 'David', ... }]
// }
Promise.withResolvers()
A new static method that provides a more convenient way to create promises with external resolve/reject functions.
// Traditional approach
function createDeferredPromise() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
// ES2024 approach
function createDeferredPromiseModern() {
return Promise.withResolvers();
}
// Practical example: Event-driven promise resolution
class EventualValue {
constructor() {
this.{ promise, resolve, reject } = Promise.withResolvers();
this.resolved = false;
}
setValue(value) {
if (!this.resolved) {
this.resolve(value);
this.resolved = true;
}
}
setError(error) {
if (!this.resolved) {
this.reject(error);
this.resolved = true;
}
}
getValue() {
return this.promise;
}
}
// Usage
const eventualValue = new EventualValue();
// Somewhere else in your code
setTimeout(() => {
eventualValue.setValue('Hello, World!');
}, 1000);
// Consumer
eventualValue.getValue().then(value => {
console.log(value); // 'Hello, World!' after 1 second
});
// Advanced example: Coordinating multiple async operations
class AsyncCoordinator {
constructor() {
this.operations = new Map();
}
createOperation(id) {
const { promise, resolve, reject } = Promise.withResolvers();
this.operations.set(id, { promise, resolve, reject });
return promise;
}
completeOperation(id, result) {
const operation = this.operations.get(id);
if (operation) {
operation.resolve(result);
this.operations.delete(id);
}
}
failOperation(id, error) {
const operation = this.operations.get(id);
if (operation) {
operation.reject(error);
this.operations.delete(id);
}
}
async waitForAll() {
const promises = Array.from(this.operations.values()).map(op => op.promise);
return Promise.all(promises);
}
}
Advanced Async Patterns
Async Iterators and Generators
// Async generator for paginated API data
async function* fetchPaginatedData(baseUrl, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
try {
const response = await fetch(`${baseUrl}?page=${page}&size=${pageSize}`);
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
yield* data.items; // Yield each item individually
page++;
hasMore = data.hasMore;
}
} catch (error) {
console.error(`Error fetching page ${page}:`, error);
hasMore = false;
}
}
}
// Usage with for-await-of
async function processAllData() {
for await (const item of fetchPaginatedData('/api/users')) {
console.log('Processing user:', item.name);
// Process item asynchronously
await processUser(item);
// Add delay to avoid overwhelming the server
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// Async iterator for real-time data streams
class RealTimeDataStream {
constructor(websocketUrl) {
this.websocketUrl = websocketUrl;
this.buffer = [];
this.waitingResolvers = [];
}
async *[Symbol.asyncIterator]() {
const ws = new WebSocket(this.websocketUrl);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (this.waitingResolvers.length > 0) {
const resolve = this.waitingResolvers.shift();
resolve({ value: data, done: false });
} else {
this.buffer.push(data);
}
};
ws.onerror = (error) => {
this.waitingResolvers.forEach(resolve =>
resolve({ value: undefined, done: true })
);
this.waitingResolvers = [];
};
try {
while (ws.readyState !== WebSocket.CLOSED) {
if (this.buffer.length > 0) {
yield this.buffer.shift();
} else {
// Wait for new data
const { value, done } = await new Promise(resolve => {
this.waitingResolvers.push(resolve);
});
if (done) break;
yield value;
}
}
} finally {
ws.close();
}
}
}
// Usage
async function consumeRealTimeData() {
const stream = new RealTimeDataStream('wss://api.example.com/stream');
for await (const data of stream) {
console.log('Received real-time data:', data);
// Process the data
await handleRealTimeData(data);
}
}
Advanced Promise Patterns
// Promise pool for controlling concurrency
class PromisePool {
constructor(concurrency = 5) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async add(promiseFactory) {
return new Promise((resolve, reject) => {
this.queue.push({
promiseFactory,
resolve,
reject
});
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
this.running++;
const { promiseFactory, resolve, reject } = this.queue.shift();
try {
const result = await promiseFactory();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process(); // Process next item in queue
}
}
async drain() {
while (this.running > 0 || this.queue.length > 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
// Usage
const pool = new PromisePool(3); // Max 3 concurrent operations
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3',
'https://api.example.com/data4',
'https://api.example.com/data5'
];
const results = await Promise.all(
urls.map(url =>
pool.add(() => fetch(url).then(r => r.json()))
)
);
// Retry with exponential backoff
async function retryWithBackoff(
operation,
maxRetries = 3,
baseDelay = 1000,
maxDelay = 10000
) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
throw error;
}
const delay = Math.min(
baseDelay * Math.pow(2, attempt),
maxDelay
);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Circuit breaker pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000, monitor = console.log) {
this.threshold = threshold;
this.timeout = timeout;
this.monitor = monitor;
this.reset();
}
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.lastFailureTime = null;
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.timeout) {
this.state = 'HALF_OPEN';
this.monitor('Circuit breaker moving to HALF_OPEN state');
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
if (this.state === 'HALF_OPEN') {
this.reset();
this.monitor('Circuit breaker reset to CLOSED state');
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.monitor('Circuit breaker opened due to failures');
}
throw error;
}
}
}
Advanced Object and Function Patterns
Proxy-based Reactive Objects
// Reactive object system using Proxy
class ReactiveObject {
constructor(target = {}) {
this.target = target;
this.listeners = new Map();
return new Proxy(target, {
get: (obj, prop) => {
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
return new ReactiveObject(obj[prop]);
}
return obj[prop];
},
set: (obj, prop, value) => {
const oldValue = obj[prop];
obj[prop] = value;
// Notify listeners
this.notifyListeners(prop, value, oldValue);
return true;
},
deleteProperty: (obj, prop) => {
const oldValue = obj[prop];
delete obj[prop];
this.notifyListeners(prop, undefined, oldValue);
return true;
}
});
}
watch(property, callback) {
if (!this.listeners.has(property)) {
this.listeners.set(property, new Set());
}
this.listeners.get(property).add(callback);
// Return unwatch function
return () => {
const callbacks = this.listeners.get(property);
if (callbacks) {
callbacks.delete(callback);
}
};
}
notifyListeners(property, newValue, oldValue) {
const callbacks = this.listeners.get(property);
if (callbacks) {
callbacks.forEach(callback => {
callback(newValue, oldValue, property);
});
}
}
}
// Usage
const state = new ReactiveObject({
user: {
name: 'John',
age: 30
},
settings: {
theme: 'dark'
}
});
// Watch for changes
const unwatchName = state.watch('user', (newUser, oldUser) => {
console.log('User changed:', newUser, oldUser);
});
state.user.name = 'Jane'; // Triggers the watcher
// Advanced validation proxy
function createValidatedObject(schema) {
return new Proxy({}, {
set(target, property, value) {
const validator = schema[property];
if (validator && !validator(value)) {
throw new Error(`Invalid value for ${property}: ${value}`);
}
target[property] = value;
return true;
}
});
}
// Usage
const userSchema = {
name: value => typeof value === 'string' && value.length > 0,
age: value => typeof value === 'number' && value >= 0 && value <= 150,
email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
const validatedUser = createValidatedObject(userSchema);
validatedUser.name = 'John'; // OK
validatedUser.age = 30; // OK
// validatedUser.age = -5; // Throws error
Advanced Function Composition
// Functional composition utilities
const pipe = (...functions) => (value) =>
functions.reduce((acc, fn) => fn(acc), value);
const compose = (...functions) => (value) =>
functions.reduceRight((acc, fn) => fn(acc), value);
// Currying utility
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
};
// Memoization with TTL
const memoizeWithTTL = (fn, ttl = 5000) => {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = fn.apply(this, args);
cache.set(key, {
value: result,
timestamp: Date.now()
});
return result;
};
};
// Debounce with immediate option
const debounce = (func, wait, immediate = false) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func(...args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func(...args);
};
};
// Throttle function
const throttle = (func, limit) => {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};
// Example usage
const add = curry((a, b) => a + b);
const multiply = curry((a, b) => a * b);
const subtract = curry((a, b) => a - b);
const add5 = add(5);
const multiplyBy2 = multiply(2);
const subtract3 = subtract(3);
// Compose operations
const complexOperation = pipe(
add5, // 10 + 5 = 15
multiplyBy2, // 15 * 2 = 30
subtract3 // 30 - 3 = 27
);
console.log(complexOperation(10)); // 27
// Memoized expensive function
const expensiveCalculation = memoizeWithTTL((n) => {
console.log(`Calculating for ${n}...`);
return n * n * n;
}, 3000);
console.log(expensiveCalculation(5)); // Calculates and logs
console.log(expensiveCalculation(5)); // Returns cached result
Modern Module Patterns
Dynamic Imports and Code Splitting
// Dynamic import with error handling
async function loadModule(moduleName) {
try {
const module = await import(`./modules/${moduleName}.js`);
return module.default || module;
} catch (error) {
console.error(`Failed to load module ${moduleName}:`, error);
throw new Error(`Module ${moduleName} not found`);
}
}
// Lazy loading with caching
class ModuleLoader {
constructor() {
this.cache = new Map();
this.loading = new Map();
}
async load(moduleName) {
// Return cached module if available
if (this.cache.has(moduleName)) {
return this.cache.get(moduleName);
}
// Return existing promise if already loading
if (this.loading.has(moduleName)) {
return this.loading.get(moduleName);
}
// Start loading
const loadPromise = this.loadModule(moduleName);
this.loading.set(moduleName, loadPromise);
try {
const module = await loadPromise;
this.cache.set(moduleName, module);
return module;
} finally {
this.loading.delete(moduleName);
}
}
async loadModule(moduleName) {
const module = await import(`./modules/${moduleName}.js`);
return module.default || module;
}
preload(moduleNames) {
return Promise.all(
moduleNames.map(name => this.load(name))
);
}
}
// Feature detection and progressive enhancement
class FeatureLoader {
constructor() {
this.features = new Map();
}
async loadFeature(featureName, fallback = null) {
try {
// Check if feature is supported
if (!this.isFeatureSupported(featureName)) {
if (fallback) {
return await this.loadModule(fallback);
}
throw new Error(`Feature ${featureName} not supported`);
}
return await this.loadModule(featureName);
} catch (error) {
console.warn(`Failed to load feature ${featureName}:`, error);
if (fallback) {
return await this.loadModule(fallback);
}
throw error;
}
}
isFeatureSupported(featureName) {
const supportMap = {
'webgl': () => !!window.WebGLRenderingContext,
'webrtc': () => !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
'serviceworker': () => 'serviceWorker' in navigator,
'webassembly': () => typeof WebAssembly === 'object'
};
const checker = supportMap[featureName];
return checker ? checker() : true;
}
async loadModule(moduleName) {
const module = await import(`./features/${moduleName}.js`);
return module.default || module;
}
}
// Usage examples
const moduleLoader = new ModuleLoader();
const featureLoader = new FeatureLoader();
// Load modules on demand
document.getElementById('chart-button').addEventListener('click', async () => {
try {
const ChartModule = await moduleLoader.load('chart');
const chart = new ChartModule.Chart('#chart-container');
chart.render();
} catch (error) {
console.error('Failed to load chart:', error);
}
});
// Progressive enhancement
async function initializeApp() {
try {
// Try to load advanced features
const AdvancedUI = await featureLoader.loadFeature('advanced-ui', 'basic-ui');
const ui = new AdvancedUI();
ui.initialize();
} catch (error) {
console.error('Failed to initialize UI:', error);
}
}
Conclusion
Modern JavaScript continues to evolve with powerful features that enable more expressive and efficient code:
- ES2024 features like array grouping and
Promise.withResolvers()
simplify common patterns - Advanced async patterns provide better control over concurrent operations
- Proxy-based reactivity enables sophisticated object observation
- Functional composition promotes reusable and testable code
- Dynamic imports enable efficient code splitting and lazy loading
The key to leveraging these features effectively is understanding their appropriate use cases and performance implications. Start with the features that solve immediate problems in your codebase, and gradually adopt more advanced patterns as your applications grow in complexity.
Remember that modern JavaScript is not just about using the latest features – it’s about writing code that is maintainable, performant, and accessible to your team and users.