JavaScript continues to evolve at a rapid pace, with new features being added annually through the ECMAScript specification. ES2024 (ES15) brings several exciting additions that can make your code more expressive, performant, and maintainable. Letโs explore the most impactful features that every developer should know.
1. Array Grouping Methods
One of the most anticipated features, array grouping methods provide a native way to group array elements without external libraries.
Object.groupBy()
const products = [
{ name: "iPhone", category: "electronics", price: 999 },
{ name: "Book", category: "education", price: 25 },
{ name: "Laptop", category: "electronics", price: 1299 },
{ name: "Course", category: "education", price: 99 },
]
// Group by category
const byCategory = Object.groupBy(products, (product) => product.category)
console.log(byCategory)
// {
// electronics: [
// { name: 'iPhone', category: 'electronics', price: 999 },
// { name: 'Laptop', category: 'electronics', price: 1299 }
// ],
// education: [
// { name: 'Book', category: 'education', price: 25 },
// { name: 'Course', category: 'education', price: 99 }
// ]
// }
// Group by price range
const byPriceRange = Object.groupBy(products, (product) => {
if (product.price < 50) return "cheap"
if (product.price < 500) return "moderate"
return "expensive"
})
Map.groupBy()
For cases where you need non-string keys:
const users = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 25 },
{ name: "Diana", age: 30 },
]
// Group by age (number keys)
const byAge = Map.groupBy(users, (user) => user.age)
console.log(byAge)
// Map(2) {
// 25 => [{ name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 }],
// 30 => [{ name: 'Bob', age: 30 }, { name: 'Diana', age: 30 }]
// }
// Group by date objects
const events = [
{ name: "Meeting", date: new Date("2024-01-15") },
{ name: "Call", date: new Date("2024-01-15") },
{ name: "Review", date: new Date("2024-01-16") },
]
const byDate = Map.groupBy(events, (event) => event.date.toDateString())
2. Promise.withResolvers()
This static method provides a more convenient way to create promises with external resolve/reject functions.
Before ES2024
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
// Later in your code
setTimeout(() => {
resolve("Success!")
}, 1000)
With Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers()
// Much cleaner! No need for variable hoisting
setTimeout(() => {
resolve("Success!")
}, 1000)
// Example: Creating a manual trigger for async operations
class AsyncButton {
constructor() {
this.clickPromise = null
this.resolveClick = null
}
waitForClick() {
if (!this.clickPromise) {
const { promise, resolve } = Promise.withResolvers()
this.clickPromise = promise
this.resolveClick = resolve
}
return this.clickPromise
}
onClick() {
if (this.resolveClick) {
this.resolveClick()
this.clickPromise = null
this.resolveClick = null
}
}
}
// Usage
const button = new AsyncButton()
button.waitForClick().then(() => {
console.log("Button was clicked!")
})
3. Temporal API (Stage 3)
While not yet finalized in ES2024, the Temporal API is making significant progress and represents the future of date/time handling in JavaScript.
Current Date Issues
// Current Date API has many issues
const date = new Date("2024-01-15")
console.log(date.getMonth()) // 0 (January is 0!)
console.log(date.getYear()) // 124 (returns year - 1900)
// Timezone issues
const utcDate = new Date("2024-01-15T10:00:00Z")
const localDate = new Date("2024-01-15T10:00:00")
// Confusing behavior with timezones
Temporal API Solution
// Import from polyfill for now
import { Temporal } from "@js-temporal/polyfill"
// Clear, unambiguous date handling
const date = Temporal.PlainDate.from("2024-01-15")
console.log(date.month) // 1 (January is 1!)
console.log(date.year) // 2024
// Explicit timezone handling
const zonedDateTime = Temporal.ZonedDateTime.from("2024-01-15T10:00:00[America/New_York]")
const utcDateTime = zonedDateTime.withTimeZone("UTC")
// Easy date arithmetic
const tomorrow = date.add({ days: 1 })
const nextWeek = date.add({ weeks: 1 })
const nextMonth = date.add({ months: 1 })
// Duration calculations
const start = Temporal.PlainDateTime.from("2024-01-15T09:00:00")
const end = Temporal.PlainDateTime.from("2024-01-15T17:30:00")
const duration = start.until(end)
console.log(duration.hours) // 8
console.log(duration.minutes) // 30
4. RegExp v Flag
The new v
flag for regular expressions provides enhanced Unicode support and set operations.
Set Operations in RegExp
// Character class subtraction
const identifier = /^[a-zA-Z_$--[0-9]]/v // Letters, underscore, $ but not numbers
console.log(identifier.test("_valid")) // true
console.log(identifier.test("9invalid")) // false
// Character class intersection
const hexDigit = /^[0-9&&[a-fA-F]]/v // Numbers that are also hex letters
console.log(hexDigit.test("5")) // false
console.log(hexDigit.test("a")) // false
console.log(hexDigit.test("f")) // false
// Note: This example shows the syntax, actual behavior may vary
// String literals in character classes
const emoji = /^[\q{๐จโ๐ป|๐ฉโ๐ป|๐งโ๐ป}]/v
console.log(emoji.test("๐จโ๐ป")) // true
Unicode Property Escapes
// Better Unicode support
const modernText = /[\p{Emoji}--\q{๐๐๐}]/v // All emoji except specific ones
const scriptText = /\p{Script=Latin}/v // Latin script characters
const mathSymbols = /\p{Math}/v // Mathematical symbols
5. Resizable ArrayBuffer and Growable SharedArrayBuffer
These features allow dynamic resizing of array buffers, providing more efficient memory management.
Resizable ArrayBuffer
// Create a resizable ArrayBuffer
const buffer = new ArrayBuffer(1024, { maxByteLength: 16384 })
console.log(buffer.byteLength) // 1024
console.log(buffer.maxByteLength) // 16384
console.log(buffer.resizable) // true
// Resize the buffer
buffer.resize(2048)
console.log(buffer.byteLength) // 2048
// Create views that work with resizing
const uint8Array = new Uint8Array(buffer, 0)
console.log(uint8Array.length) // 2048
// Resize again
buffer.resize(4096)
console.log(uint8Array.length) // 4096 (view automatically updates)
// Practical example: Dynamic binary data handling
class DynamicBinaryData {
constructor(initialSize = 1024, maxSize = 1024 * 1024) {
this.buffer = new ArrayBuffer(initialSize, { maxByteLength: maxSize })
this.view = new DataView(this.buffer)
this.position = 0
}
ensureCapacity(needed) {
const available = this.buffer.byteLength - this.position
if (available < needed) {
const newSize = Math.min(this.buffer.maxByteLength, Math.max(this.buffer.byteLength * 2, this.position + needed))
this.buffer.resize(newSize)
}
}
writeUint32(value) {
this.ensureCapacity(4)
this.view.setUint32(this.position, value)
this.position += 4
}
writeString(str) {
const encoder = new TextEncoder()
const encoded = encoder.encode(str)
this.ensureCapacity(4 + encoded.length)
this.view.setUint32(this.position, encoded.length)
this.position += 4
new Uint8Array(this.buffer, this.position, encoded.length).set(encoded)
this.position += encoded.length
}
}
6. Import Attributes
Import attributes (formerly import assertions) provide a way to specify additional information about module imports.
JSON Imports
// Import JSON with type assertion
import packageInfo from './package.json' with { type: 'json' };
console.log(packageInfo.version);
// Dynamic imports with attributes
const config = await import('./config.json', {
with: { type: 'json' }
});
// CSS imports (future feature)
import styles from './styles.css' with { type: 'css' };
document.adoptedStyleSheets = [styles];
WebAssembly Imports
// Import WebAssembly modules
import wasmModule from './math.wasm' with { type: 'webassembly' };
const { add, multiply } = wasmModule;
console.log(add(5, 3)); // 8
console.log(multiply(4, 7)); // 28
7. ArrayBuffer Transfer
The transfer
method allows transferring ownership of ArrayBuffer data efficiently.
// Create an ArrayBuffer
const originalBuffer = new ArrayBuffer(1024)
const view = new Uint8Array(originalBuffer)
view[0] = 42
console.log(originalBuffer.byteLength) // 1024
console.log(view[0]) // 42
// Transfer to a new buffer (can change size)
const newBuffer = originalBuffer.transfer(2048)
const newView = new Uint8Array(newBuffer)
console.log(originalBuffer.byteLength) // 0 (detached)
console.log(newBuffer.byteLength) // 2048
console.log(newView[0]) // 42 (data preserved)
// Original buffer is now detached
try {
console.log(view[0]) // Throws TypeError
} catch (e) {
console.log("Original buffer is detached")
}
// Practical use: Efficient buffer management in workers
class BufferManager {
constructor() {
this.buffer = new ArrayBuffer(1024)
}
expandAndTransfer(newSize) {
this.buffer = this.buffer.transfer(newSize)
return this.buffer
}
sendToWorker(worker) {
const transferred = this.buffer.transfer()
worker.postMessage({ buffer: transferred }, [transferred])
// this.buffer is now detached, need to create new one
this.buffer = new ArrayBuffer(1024)
}
}
8. Well-Formed Unicode Strings
New methods to handle Unicode strings more robustly.
String.prototype.isWellFormed()
const validString = "Hello ๐ World"
const invalidString = "Invalid \uD800 string" // Lone surrogate
console.log(validString.isWellFormed()) // true
console.log(invalidString.isWellFormed()) // false
// Practical use: Input validation
function sanitizeInput(input) {
if (!input.isWellFormed()) {
return input.toWellFormed()
}
return input
}
String.prototype.toWellFormed()
const malformed = "Text with \uD800 lone surrogate"
const wellFormed = malformed.toWellFormed()
console.log(malformed.isWellFormed()) // false
console.log(wellFormed.isWellFormed()) // true
console.log(wellFormed) // 'Text with ๏ฟฝ lone surrogate'
// Use in data processing pipelines
class TextProcessor {
static clean(text) {
return text.isWellFormed() ? text : text.toWellFormed()
}
static processUserInput(input) {
const cleaned = this.clean(input)
// Continue with processing...
return cleaned.trim().toLowerCase()
}
}
Browser Support and Polyfills
Feature Detection
// Check for array grouping support
const supportsGroupBy = typeof Object.groupBy === "function"
if (!supportsGroupBy) {
// Load polyfill or use alternative implementation
import("core-js/proposals/array-grouping")
}
// Check for Promise.withResolvers
const supportsWithResolvers = typeof Promise.withResolvers === "function"
if (!supportsWithResolvers) {
Promise.withResolvers = function () {
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
}
Gradual Adoption Strategy
// Use feature detection for progressive enhancement
class ModernArrayUtils {
static groupBy(array, keyFn) {
if (typeof Object.groupBy === "function") {
return Object.groupBy(array, keyFn)
}
// Fallback implementation
return array.reduce((groups, item) => {
const key = keyFn(item)
groups[key] = groups[key] || []
groups[key].push(item)
return groups
}, {})
}
static createDeferredPromise() {
if (typeof Promise.withResolvers === "function") {
return Promise.withResolvers()
}
// Fallback
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
}
Real-World Applications
Data Processing Pipeline
class DataProcessor {
constructor() {
this.buffer = new ArrayBuffer(1024, { maxByteLength: 1024 * 1024 })
this.processing = false
}
async processData(rawData) {
if (this.processing) {
const { promise } = Promise.withResolvers()
this.queue = this.queue || []
this.queue.push({ data: rawData, promise })
return promise
}
this.processing = true
try {
// Group data by type
const grouped = Object.groupBy(rawData, (item) => item.type)
// Process each group
const results = {}
for (const [type, items] of Object.entries(grouped)) {
results[type] = await this.processGroup(items)
}
return results
} finally {
this.processing = false
this.processQueue()
}
}
async processGroup(items) {
// Ensure buffer capacity
const needed = items.length * 64 // Estimate
if (this.buffer.byteLength < needed) {
this.buffer.resize(Math.min(needed, this.buffer.maxByteLength))
}
// Process items...
return items.map((item) => this.processItem(item))
}
processQueue() {
if (this.queue && this.queue.length > 0) {
const { data, promise } = this.queue.shift()
this.processData(data).then(promise.resolve).catch(promise.reject)
}
}
}
Form Validation with Modern Features
class ModernFormValidator {
constructor() {
this.rules = new Map()
this.validators = this.createValidators()
}
createValidators() {
return {
unicode: /^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Z}]*$/v,
email: /^[\w._%+-]+@[\w.-]+\.[A-Z]{2,}$/i,
strongPassword: /^(?=.*[\p{Ll}])(?=.*[\p{Lu}])(?=.*[\p{N}])(?=.*[\p{P}])[\s\S]{8,}$/v,
}
}
async validateForm(formData) {
// Group validation rules by field
const rulesByField = Object.groupBy(Array.from(this.rules.entries()), ([field]) => field.split(".")[0])
const { promise, resolve, reject } = Promise.withResolvers()
const errors = {}
let hasErrors = false
for (const [field, value] of Object.entries(formData)) {
// Ensure text is well-formed Unicode
const cleanValue = typeof value === "string" ? (value.isWellFormed() ? value : value.toWellFormed()) : value
// Validate field
const fieldErrors = this.validateField(field, cleanValue, rulesByField[field] || [])
if (fieldErrors.length > 0) {
errors[field] = fieldErrors
hasErrors = true
}
}
// Resolve asynchronously
setTimeout(() => {
if (hasErrors) {
reject(new ValidationError("Form validation failed", errors))
} else {
resolve(formData)
}
})
return promise
}
validateField(field, value, rules) {
const errors = []
for (const [ruleName, rule] of rules) {
if (typeof rule === "function") {
const result = rule(value)
if (!result.valid) {
errors.push(result.message)
}
} else if (rule instanceof RegExp) {
if (!rule.test(value)) {
errors.push(`${field} does not match required pattern`)
}
}
}
return errors
}
}
class ValidationError extends Error {
constructor(message, errors) {
super(message)
this.name = "ValidationError"
this.errors = errors
}
}
Migration Strategy
1. Start with Non-Breaking Features
Begin with features that donโt require syntax changes:
// Easy wins - these work alongside existing code
const grouped = Object.groupBy(array, keyFn)
const { promise, resolve, reject } = Promise.withResolvers()
const isValid = str.isWellFormed()
2. Progressive Enhancement
// Feature-detect and enhance
function createGroupedData(data, groupFn) {
if (typeof Object.groupBy === "function") {
return Object.groupBy(data, groupFn)
}
// Fallback to reduce
return data.reduce((acc, item) => {
const key = groupFn(item)
acc[key] = acc[key] || []
acc[key].push(item)
return acc
}, {})
}
3. Gradual Adoption
// Use build-time feature detection
const config = {
useNativeGroupBy: typeof Object.groupBy === "function",
useTemporalAPI: typeof Temporal !== "undefined",
useResizableBuffers: (() => {
try {
new ArrayBuffer(1, { maxByteLength: 2 })
return true
} catch {
return false
}
})(),
}
export default config
Conclusion
ES2024 brings practical, developer-friendly features that address real pain points in JavaScript development. The array grouping methods eliminate the need for external libraries for common operations, Promise.withResolvers simplifies async patterns, and Unicode improvements make text handling more robust.
While browser support is still developing, these features are designed with backward compatibility in mind. Start experimenting with polyfills and feature detection, and gradually adopt these features as browser support improves.
The JavaScript ecosystem continues to mature, and these additions demonstrate the languageโs commitment to developer experience and practical utility. Stay current with these features to write more expressive, maintainable, and performant JavaScript code.
Key takeaways:
- Array grouping eliminates common reduce patterns
- Promise.withResolvers simplifies external promise control
- Unicode improvements make text handling more robust
- Import attributes provide better module metadata
- Buffer improvements enable more efficient memory management
Embrace these features gradually, and youโll find your JavaScript code becoming more expressive and maintainable than ever before.