Profile

The Storyteller

Minimal musings on code, design, and life


Modern JavaScript: ES2024 Features Every Developer Should Know

By Hasin Hayder May 12, 2025 Posted in JavaScript
Modern JavaScript: ES2024 Features Every Developer Should Know

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:

Embrace these features gradually, and youโ€™ll find your JavaScript code becoming more expressive and maintainable than ever before.