Guides writing clean, performant JavaScript using features from ES6 through ES2026. Includes decision trees for array methods, nullish handling, and immutable operations with runtime support notes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ccheney-robust-skills:modern-javascriptThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write clean, performant, maintainable JavaScript using modern language features. This skill covers ES6 through ES2026, emphasizing immutability, functional patterns, and expressive syntax — and it pins each feature to the ECMAScript edition and runtimes that actually support it, because misattributed versions are the most common way modern-JS advice breaks.
Write clean, performant, maintainable JavaScript using modern language features. This skill covers ES6 through ES2026, emphasizing immutability, functional patterns, and expressive syntax — and it pins each feature to the ECMAScript edition and runtimes that actually support it, because misattributed versions are the most common way modern-JS advice breaks.
What do I need?
├─ Transform each element → .map()
├─ Keep some elements → .filter()
├─ Find one element → .find() / .findLast()
├─ Check if condition met → .some() / .every()
├─ Reduce to single value → .reduce()
├─ Get last element → .at(-1)
├─ Sort without mutating → .toSorted()
├─ Reverse without mutating → .toReversed()
├─ Group by property → Object.groupBy() / Map.groupBy()
├─ Collect an async iterable → Array.fromAsync()
└─ Flatten nested arrays → .flat() / .flatMap()
Nullish handling?
├─ Safe property access → obj?.prop / obj?.[key]
├─ Safe method call → obj?.method?.()
├─ Default for null/undefined only → value ?? 'default'
├─ Default for any falsy → value || 'default'
├─ Assign if null/undefined → obj.prop ??= 'default'
└─ Check property exists → Object.hasOwn(obj, 'key')
Always prefer non-mutating methods:
├─ Sort array → .toSorted() (not .sort())
├─ Reverse array → .toReversed() (not .reverse())
├─ Splice array → .toSpliced() (not .splice())
├─ Update element → .with(i, val) (not arr[i] = val)
├─ Add to array → [...arr, item] (not .push())
└─ Merge objects → {...obj, key} (not Object.assign())
Editions are set by which proposals reach TC39 Stage 4 before the spring cutoff, so a feature's edition often lags its browser availability (e.g. Array.fromAsync shipped in browsers in 2023-24 but is spec'd in ES2026). Cite editions from this table, not from memory.
| Version | Year | Key Features |
|---|---|---|
| ES6 | 2015 | let/const, arrow functions, classes, destructuring, spread, Promises, modules, Symbol, Map/Set, Proxy, generators |
| ES2016 | 2016 | Array.includes(), exponentiation operator ** |
| ES2017 | 2017 | async/await, Object.values/entries, padStart/padEnd, trailing commas, SharedArrayBuffer, Atomics |
| ES2018 | 2018 | Rest/spread for objects, for await...of, Promise.finally(), RegExp named groups, lookbehind, dotAll flag |
| ES2019 | 2019 | .flat(), .flatMap(), Object.fromEntries(), trimStart/End(), optional catch binding, stable Array.sort() |
| ES2020 | 2020 | Optional chaining ?., nullish coalescing ??, BigInt, Promise.allSettled(), globalThis, dynamic import(), matchAll |
| ES2021 | 2021 | String.replaceAll(), Promise.any(), logical assignment ??= ||= &&=, numeric separators 1_000_000, WeakRef |
| ES2022 | 2022 | .at(), Object.hasOwn(), top-level await, private class fields #field, static blocks, Error cause, /d flag |
| ES2023 | 2023 | .toSorted(), .toReversed(), .toSpliced(), .with(), .findLast(), .findLastIndex(), hashbang grammar |
| ES2024 | 2024 | Object.groupBy(), Map.groupBy(), Promise.withResolvers(), RegExp /v flag, resizable ArrayBuffer + transfer(), Atomics.waitAsync(), isWellFormed() |
| ES2025 | 2025 | Set methods (.union, .intersection, ...), iterator helpers (.map, .filter, .take), RegExp.escape(), Promise.try(), import attributes + JSON modules, duplicate named capture groups, RegExp inline modifiers, Float16Array |
| ES2026 | 2026 | Array.fromAsync(), Error.isError(), Math.sumPrecise(), Uint8Array.fromBase64()/toBase64(), Iterator.concat(), Map.getOrInsert(), JSON.parse source access |
Already Stage 4 and slated for ES2027: Temporal, explicit resource management (using/await using), Atomics.pause(), Iterator.zip(). Decorators remain Stage 3 (transpiler only). Records & Tuples (#{}/#[]) was withdrawn — never emit that syntax. See references/UPCOMING.md.
State the baseline when reaching for newer features; suggesting .toSorted() to someone on Node 18 produces a runtime TypeError: arr.toSorted is not a function.
Promise.withResolvers and Array.fromAsync (Node 21-22+, Safari 16.4-17.4+).using/await using, Error.isError, Uint8Array base64.Per-feature version tables live at the end of references/ES2024.md, references/ES2025.md, and references/ES2026.md.
// ❌ Legacy
const last = arr[arr.length - 1];
const secondLast = arr[arr.length - 2];
// ✅ Modern (ES2022)
const last = arr.at(-1);
const secondLast = arr.at(-2);
// ❌ Mutates original array (and returns the same reference)
const sorted = arr.sort((a, b) => a - b);
const reversed = arr.reverse();
// ✅ Returns new array (ES2023)
const sorted = arr.toSorted((a, b) => a - b);
const reversed = arr.toReversed();
const updated = arr.with(2, 'new value');
const removed = arr.toSpliced(1, 1);
// ❌ Legacy with regex
const result = str.replace(/foo/g, 'bar');
// ✅ Modern (ES2021) — no escaping worries for literal strings
const result = str.replaceAll('foo', 'bar');
// ❌ Manual grouping
const grouped = items.reduce((acc, item) => {
const key = item.category;
acc[key] = acc[key] || [];
acc[key].push(item);
return acc;
}, {});
// ✅ Modern (ES2024)
const grouped = Object.groupBy(items, item => item.category);
Gotcha: Object.groupBy returns a null-prototype object — property access and destructuring work, but grouped.hasOwnProperty(...) throws. Use Object.hasOwn(). Use Map.groupBy when keys are objects or non-strings. There is no Array.prototype.group() — that earlier shape of the proposal never shipped.
// ❌ Falsy check (0, '', false are valid values)
const value = input || 'default';
const name = user && user.profile && user.profile.name;
// ✅ Nullish check (only null/undefined)
const value = input ?? 'default';
const name = user?.profile?.name;
// ❌ Can be fooled by prototype or overwritten hasOwnProperty
if (obj.hasOwnProperty('key')) { }
// ✅ Modern (ES2022) — also works on null-prototype objects
if (Object.hasOwn(obj, 'key')) { }
// ❌ Verbose assignment
if (obj.prop === null || obj.prop === undefined) {
obj.prop = 'default';
}
// ✅ Modern (ES2021)
obj.prop ??= 'default'; // Assign if null/undefined
obj.count ||= 0; // Assign if falsy
obj.enabled &&= check(); // Assign if truthy
// Wait for all, fail if any fails
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
// Wait for all, get status of each
const results = await Promise.allSettled([fetchA(), fetchB()]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
// First to succeed
const fastest = await Promise.any([fetchFromCDN1(), fetchFromCDN2()]);
// First to settle
const winner = await Promise.race([fetchData(), timeout(5000)]);
// ❌ Legacy pattern
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// ✅ Modern (ES2024)
const { promise, resolve, reject } = Promise.withResolvers();
// In ES modules, await at top level
const config = await fetch('/config.json').then(r => r.json());
const db = await connectDatabase(config);
export { db };
// Add/update property
const updated = { ...user, age: 31 };
// Remove property
const { password, ...userWithoutPassword } = user;
// Nested update
const newState = {
...state,
user: { ...state.user, name: 'New Name' }
};
// Chain transformations (ES2023)
const result = users
.filter(u => u.active)
.map(u => u.name)
.toSorted();
// Using flatMap for filter+map (single pass)
const activeNames = users.flatMap(u => u.active ? [u.name] : []);
// ES2024: Group then process
const byStatus = Object.groupBy(users, u => u.active ? 'active' : 'inactive');
const activeUserNames = byStatus.active?.map(u => u.name) ?? [];
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const processUser = pipe(
user => ({ ...user, name: user.name.trim() }),
user => ({ ...user, email: user.email.toLowerCase() }),
user => ({ ...user, createdAt: new Date() })
);
// Basic with rename and default
const { name: userName, age = 18 } = user;
// Nested
const { address: { city, country } } = user;
// Rest
const { id, ...userData } = user;
// Skip elements
const [first, , third] = array;
// Rest
const [head, ...tail] = array;
// Swap variables
[a, b] = [b, a];
// Function returns
const [x, y] = getCoordinates();
| Anti-Pattern | Problem | Modern Solution |
|---|---|---|
arr[arr.length-1] | Verbose, error-prone | arr.at(-1) |
.sort() on original | Mutates array | .toSorted() |
.replace(/x/g) for literal strings | Needs regex escaping | .replaceAll() |
obj.hasOwnProperty() | Can be overwritten; throws on null-prototype objects | Object.hasOwn() |
value || default | 0, '', false treated as falsy | value ?? default |
obj && obj.prop && obj.prop.method() | Verbose null checks | obj?.prop?.method?.() |
for (let i = 0; ...) | Index bugs, verbose | .map(), .filter(), for...of |
items.forEach(async item => ...) | forEach ignores promises; nothing is awaited | for...of + await, or Promise.all(items.map(...)) |
let resolve; new Promise(r => ...) | Boilerplate, escape-hatch closures | Promise.withResolvers() |
| Manual array grouping with reduce | Verbose, error-prone | Object.groupBy() / Map.groupBy() |
#{ } / #[ ] record/tuple literals | Proposal withdrawn April 2025; never valid syntax | Plain frozen objects, or Maps keyed manually |
const by default — Only use let when reassignment is needed.toSorted(), .toReversed(), spread operator?? for defaults, not || (unless intentional).map(), .filter(), .find() over loopsasync/await — Instead of .then() chainsOpen the reference that matches the task; SKILL.md alone is enough for everyday syntax choices.
| Read this | When |
|---|---|
| references/ES2016-ES2017.md | async/await fundamentals, Object.values/entries, string padding |
| references/ES2018-ES2019.md | Object rest/spread, flat/flatMap, regex named groups/lookbehind/dotAll |
| references/ES2022-ES2023.md | .at(), change-by-copy methods, private fields, static blocks, error cause |
| references/ES2024.md | Grouping data, Promise.withResolvers, /v regex, ArrayBuffer resize/transfer |
| references/ES2025.md | Set operations, iterator helpers, RegExp.escape, Promise.try, import attributes |
| references/ES2026.md | Array.fromAsync, Error.isError, base64 bytes, Math.sumPrecise, Map.getOrInsert |
| references/UPCOMING.md | Temporal, using/await using, decorators, proposal status questions |
| Read this | When |
|---|---|
| references/PROMISES.md | Writing async flows, choosing combinators, fixing promise anti-patterns |
| references/CONCURRENCY.md | Rate limiting, pools, retry/backoff, timeouts, cancellation, async iteration |
| references/IMMUTABILITY.md | State updates (React/Redux), pure functions, deep clone decisions |
| references/COMPOSITION.md | Higher-order functions, currying, memoization, Maybe/Result patterns |
| references/CHEATSHEET.md | Quick syntax lookup across all editions |
npx claudepluginhub ccheney/robust-skillsGuides use of ES6+ features (async/await, destructuring, spread, arrow functions, promises, modules, iterators, generators) and functional programming patterns for clean, efficient JavaScript. Use when refactoring legacy code or optimizing apps.
Guides refactoring legacy JavaScript to modern ES6+ syntax, implementing functional programming patterns, and writing clean, maintainable, performant code.
Guides usage of ES6+ features (async/await, destructuring, spread, arrow functions, promises, modules) and functional patterns for refactoring legacy code and writing clean JavaScript.