All skills
ccheney avatar

/modern-javascript

@00695ba

Implement or debug JavaScript language behavior, including promise completion, async iteration, mutation, and runtime compatibility. Use for ECMAScript semantics or polyfill/transform choices; not unrelated framework or TypeScript type design.

Use this Skill: https://skilld.dev/gh/ccheney/robust-skills/modern-javascript

This session only. Nothing lands on disk.

referencesES2024.md

≈1.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

ES2024 Features

Object.groupBy(), Map.groupBy(), Promise.withResolvers(), String.isWellFormed(), String.toWellFormed(), RegExp v flag (Unicode sets), resizable ArrayBuffers, ArrayBuffer.transfer(), Atomics.waitAsync().

Note: Array.fromAsync() is often mislabeled as ES2024 — it reached Stage 4 in 2025 and is part of ES2026 (see ES2026.md), even though runtimes have shipped it since 2023-24.

Contents

Object.groupBy() and Map.groupBy()

Group array (or any iterable) elements by a key. These are static methods on Object and Map — there is no Array.prototype.group(); that earlier form of the proposal never shipped.

const inventory = [
  { name: 'asparagus', type: 'vegetable', quantity: 5 },
  { name: 'banana', type: 'fruit', quantity: 0 },
  { name: 'goat', type: 'meat', quantity: 23 },
  { name: 'cherry', type: 'fruit', quantity: 5 },
  { name: 'fish', type: 'meat', quantity: 22 }
];

// Group by type
const byType = Object.groupBy(inventory, item => item.type);
/*
{
  vegetable: [{ name: 'asparagus', ... }],
  fruit: [{ name: 'banana', ... }, { name: 'cherry', ... }],
  meat: [{ name: 'goat', ... }, { name: 'fish', ... }]
}
*/

// Group by availability
const byAvailability = Object.groupBy(inventory, item =>
  item.quantity > 0 ? 'inStock' : 'outOfStock'
);

// Map.groupBy returns a Map instead of a plain object
const byTypeMap = Map.groupBy(inventory, item => item.type);
byTypeMap.get('fruit');  // [{ name: 'banana', ... }, { name: 'cherry', ... }]

Gotcha — the return value is a null-prototype object. Object.groupBy returns an object created with Object.create(null). Property access, spread, and destructuring all work, but inherited Object.prototype methods do not exist on it:

const groups = Object.groupBy([1, 2, 3], n => n % 2 ? 'odd' : 'even');

groups.odd;                        // [1, 3] ✓
Object.hasOwn(groups, 'odd');      // true ✓
// groups.hasOwnProperty('odd');   // TypeError: not a function
// `${groups}`;                    // TypeError: cannot convert to primitive

Migration from manual grouping:

// ❌ Before
const grouped = items.reduce((acc, item) => {
  const key = item.category;
  if (!acc[key]) acc[key] = [];
  acc[key].push(item);
  return acc;
}, {});
// ✅ After
const grouped = Object.groupBy(items, item => item.category);

When to use Map.groupBy:

// Use Map.groupBy when keys are non-strings. Object.groupBy coerces
// every key to a property key (string or symbol); Map keeps identity.
const byDate = Map.groupBy(events, event => event.date);
// Keys are Date objects — but note Map compares object keys by
// identity, so use a primitive (e.g. an ISO date string) when two equal
// dates should land in the same group.

// Map is also the right target when you'll iterate groups in order
const byPriority = Map.groupBy(tasks, task => task.priority);

Promise.withResolvers()

Create a Promise with externally accessible resolve/reject.

// ❌ Before: Awkward closure pattern
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
// Use resolve/reject somewhere else
resolve('done');
// ✅ After: Clean destructuring
const { promise, resolve, reject } = Promise.withResolvers();
// Use anywhere
setTimeout(() => resolve('done'), 1000);
await promise;  // 'done'

Practical example: Event-to-Promise:

function waitForEvent(element, eventName) {
  const { promise, resolve } = Promise.withResolvers();
  element.addEventListener(eventName, resolve, { once: true });
  return promise;
}

const button = document.querySelector('button');
await waitForEvent(button, 'click');
console.log('Button was clicked!');

Practical example: Manual async control:

class AsyncQueue {
  #queue = [];
  #waiters = [];

  async next() {
    if (this.#queue.length > 0) {
      return this.#queue.shift();
    }
    const waiter = Promise.withResolvers();
    this.#waiters.push(waiter.resolve);
    return waiter.promise;
  }

  push(item) {
    const resolve = this.#waiters.shift();
    if (resolve) {
      resolve(item);
    } else {
      this.#queue.push(item);
    }
  }
}

Well-Formed Unicode Strings

Check and fix malformed UTF-16 strings.

// Check if string is well-formed
const valid = 'Hello 👋';
const invalid = 'Hi \uD800';  // Lone surrogate

valid.isWellFormed();    // true
invalid.isWellFormed();  // false

// Fix malformed strings
invalid.toWellFormed();  // 'Hi �' (replacement character)

// Use before encoding for URLs/APIs — encodeURIComponent throws on lone surrogates
function safeEncode(str) {
  return encodeURIComponent(str.toWellFormed());
}

RegExp v Flag (Unicode Sets)

Enhanced Unicode handling in regex: set operations in character classes, string properties, nested classes. /v is a superset of /u — the two flags cannot be combined.

// Set operations in character classes
const emoji = /[\p{Emoji}--\p{ASCII}]/v;  // Emoji minus ASCII

// String properties (multi-code-point matches, /v only)
const flags = /\p{RGI_Emoji}/v;  // Matches full emoji sequences like 🇺🇸

// Intersection
const pattern = /[[a-z]&&[^aeiou]]/v;  // Consonants only

// Nested character classes
const complex = /[[0-9]--[0-4]]/v;  // 5-9 only

Resizable ArrayBuffers and transfer()

// Create resizable buffer
const buffer = new ArrayBuffer(1024, { maxByteLength: 4096 });

console.log(buffer.byteLength);     // 1024
console.log(buffer.maxByteLength);  // 4096
console.log(buffer.resizable);      // true

// Resize within bounds
buffer.resize(2048);
console.log(buffer.byteLength);     // 2048

// Transfer ownership and detach the source buffer
const newBuffer = buffer.transfer(512);
console.log(buffer.detached);        // true
console.log(newBuffer.byteLength);   // 512

Atomics.waitAsync()

Asynchronous shared memory waiting.

const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

// Async wait (doesn't block main thread)
const result = Atomics.waitAsync(sharedArray, 0, 0);
const status = result.async ? await result.value : result.value;
console.log('Wait result:', status); // 'ok', 'timed-out', or 'not-equal'

// In another context/worker
Atomics.notify(sharedArray, 0);

Browser/Node.js Support

First versions with unflagged support (source: MDN browser-compat-data, 2026):

Feature Chrome Firefox Safari Node.js
Object.groupBy / Map.groupBy 117+ 119+ 17.4+ 21+
Promise.withResolvers 119+ 121+ 17.4+ 22+
String.isWellFormed/toWellFormed 111+ 119+ 16.4+ 20+
RegExp v flag 112+ 116+ 17+ 20+
Resizable ArrayBuffer 111+ 128+ 16.4+ 20+
ArrayBuffer.transfer 114+ 122+ 17.4+ 21+
Atomics.waitAsync 90+ 145+ 16.4+ 16+

All of these are in Node 22 and current evergreen browsers, but older WebViews and embedded runtimes may lag. Re-check MDN Browser Compatibility Data against the project's targets.

Source: SKILL.md on GitHub

No alerts14d4 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    The skill provides a technical reference for modern JavaScript features and toolchains. It includes an indirect prompt injection surface by directing the agent to read and analyze project files, such as lockfiles and build configurations, which may contain untrusted data that could influence the agent's code generation logic.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer7mo

    12 files scanned · No issues

Signed by skilld at 00695ba. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 3 weeks ago

README badge

README badge for ccheney/robust-skills/modern-javascript