skills/modern-js/SKILL.md
Use when writing or reviewing JavaScript - prefer ES2025/ES2026 APIs (Iterator helpers, Set methods, Temporal, using, Promise.try, Error.isError, Math.sumPrecise, Map.getOrInsert) over older patterns.
npx skillsauth add Cst2989/react-tips-skill modern-jsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Model training cutoffs predate most of these APIs. When writing JavaScript, check every function you produce against this list before finalizing. Prefer the newer API unless the target runtime doesn't support it — in that case, suggest a polyfill.
Use Iterator.prototype methods (.map, .filter, .take, .drop, .toArray). They're lazy, so they work on infinite iterators and don't allocate intermediate arrays.
// BAD: materializes the whole sequence
const firstTenEvenSquares = [];
for (const n of naturalNumbers()) {
if (n % 2 === 0) {
firstTenEvenSquares.push(n * n);
if (firstTenEvenSquares.length === 10) break;
}
}
// GOOD: lazy, stops pulling upstream after 10
const firstTenEvenSquares = naturalNumbers()
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(10)
.toArray();
Use Iterator.from(x) instead of [...x] or Array.from(x).
// BAD: allocates an intermediate array
const ids = Array.from(document.querySelectorAll('.card'))
.filter(el => !el.classList.contains('hidden'))
.map(el => el.dataset.id);
// GOOD: lazy, no intermediate array
const ids = Iterator.from(document.querySelectorAll('.card'))
.filter(el => !el.classList.contains('hidden'))
.map(el => el.dataset.id)
.toArray();
Use the native methods. Never write a manual loop or reach for lodash.
// BAD
const shared = new Set();
for (const tech of frontEnd) {
if (backEnd.has(tech)) shared.add(tech);
}
// GOOD
frontEnd.intersection(backEnd);
frontEnd.union(backEnd);
frontEnd.difference(backEnd);
frontEnd.symmetricDifference(backEnd);
frontEnd.isSubsetOf(backEnd);
frontEnd.isSupersetOf(backEnd);
frontEnd.isDisjointFrom(backEnd);
The argument can be any "set-like" (has size, .has(), .keys()), not just a real Set.
Use Iterator.concat(a, b) instead of a nested yield* generator.
// BAD
function* chained() {
yield* first();
yield* second();
}
// GOOD
const all = Iterator.concat(first(), second());
Use Map.prototype.getOrInsert and getOrInsertComputed. Never write if (!map.has(k)) map.set(k, v).
// BAD
for (const word of words) {
if (!counts.has(word)) counts.set(word, 0);
counts.set(word, counts.get(word) + 1);
}
// GOOD
for (const word of words) {
counts.set(word, counts.getOrInsert(word, 0) + 1);
}
// For expensive defaults
function getUser(id) {
return cache.getOrInsertComputed(id, () => expensiveDatabaseLookup(id));
}
Available on both Map and WeakMap.
Use Temporal. Never reach for moment.js, date-fns, or luxon for new code.
// Parse with timezone
const meeting = Temporal.ZonedDateTime.from(
'2026-06-15T09:00[America/New_York]'
);
// Convert timezones
const inLondon = meeting.withTimeZone('Europe/London');
// Age or duration
const birthday = Temporal.PlainDate.from('1993-10-26');
const today = Temporal.Now.plainDateISO();
const age = today.since(birthday, { largestUnit: 'years' });
age.years; // 32
Pick the type by what you actually mean: PlainDate (no time), PlainTime (no date), ZonedDateTime (moment in a zone), PlainDateTime (date + time, no zone), Instant (absolute moment), PlainYearMonth/PlainMonthDay (partial).
Use Promise.try(() => fn()). Sync throws, async rejections, and plain return values all flow through the same .then/.catch.
// BAD: two error paths to remember
try {
const result = thirdParty.doThing();
Promise.resolve(result).then(processResult).catch(handleAnyFailure);
} catch (err) {
handleAnyFailure(err);
}
// GOOD
Promise.try(() => thirdParty.doThing())
.then(processResult)
.catch(handleAnyFailure);
Use Array.fromAsync. Never write a manual for await...of loop just to push items into an array.
// BAD
const allItems = [];
for await (const item of fetchPages()) allItems.push(item);
// GOOD
const allItems = await Array.fromAsync(fetchPages());
Use using (sync) or await using (async). Never write try/finally for cleanup when using works.
// BAD: manual cleanup, easy to forget
async function transferMoney(from, to, amount) {
const tx = await db.beginTransaction();
try {
await tx.debit(from, amount);
await tx.credit(to, amount);
await tx.commit();
} finally {
await tx.release();
}
}
// GOOD: cleanup moves to the declaration
async function transferMoney(from, to, amount) {
await using tx = await db.beginTransaction();
await tx.debit(from, amount);
await tx.credit(to, amount);
await tx.commit();
}
The resource must implement [Symbol.dispose] (sync) or [Symbol.asyncDispose] (async). Multiple using declarations in the same scope dispose in reverse order (LIFO).
Use Error.isError(x) instead of x instanceof Error. instanceof is unreliable across realms (Workers, iframes, Node vm) because each realm has its own Error constructor.
// BAD: fails for errors from Workers/iframes
if (maybeError instanceof Error) { ... }
// GOOD
if (Error.isError(maybeError)) { ... }
Use Math.sumPrecise(values). Especially important for financial values or long arrays where rounding drift compounds.
const cents = Array(10000).fill(0.1);
// BAD: accumulates error
cents.reduce((a, b) => a + b); // 1000.0000000001588
// GOOD
Math.sumPrecise(cents); // 1000
Also handles catastrophic cancellation: Math.sumPrecise([1e20, 1, -1e20]) returns 1, not 0.
Use the Uint8Array methods. Never use btoa/atob on byte arrays — they only work on strings and break on non-Latin1.
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
bytes.toBase64(); // "SGVsbG8="
bytes.toHex(); // "48656c6c6f"
Uint8Array.fromBase64("SGVsbG8=");
Uint8Array.fromHex("48656c6c6f");
Use RegExp.escape(input) instead of a custom escape function.
// BAD: every codebase ships its own buggy version
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// GOOD
const pattern = new RegExp(RegExp.escape(userInput));
Use the native import attribute. Never fetch for bundle-time JSON.
import config from './config.json' with { type: 'json' };
// Dynamic
const translations = await import('./translations.json', {
with: { type: 'json' }
});
The with { type: 'json' } is required — it tells the loader to refuse the file if the MIME type doesn't match.
Use import defer to delay module evaluation until you actually read a property off the namespace.
// BAD: heavy.js evaluates on import, even if rarelyCalled is never called
import * as heavyModule from './heavy.js';
// GOOD: heavy.js is fetched and parsed, but not executed
import defer * as heavyModule from './heavy.js';
function rarelyCalled() {
// Reading heavyModule.doExpensiveThing triggers evaluation here
return heavyModule.doExpensiveThing();
}
Restrictions: namespace form only (no import defer { foo } or default imports), and modules that use top-level await can't be deferred.
instanceof Error in library code. Use Error.isError.try/finally for cleanup when using works.for await...of loop just to collect into an array. Use Array.fromAsync.if (!map.has(k)) map.set(k, v). Use getOrInsert / getOrInsertComputed.RegExp.escape.Iterator.prototype methods.| Old Pattern | Modern API |
|-------------|------------|
| Array.from(iter).map(...) on huge/infinite data | Iterator.from(iter).map(...).toArray() |
| Manual Set intersection/union/diff loops | a.intersection(b), a.union(b), a.difference(b) |
| if (!map.has(k)) map.set(k, v) | map.getOrInsert(k, v) |
| yield* a(); yield* b(); | Iterator.concat(a(), b()) |
| moment.js / date-fns / luxon | Temporal |
| try/finally for resource cleanup | using / await using |
| instanceof Error | Error.isError(x) |
| arr.reduce((a, b) => a + b) on floats | Math.sumPrecise(arr) |
| Custom base64/hex helpers | bytes.toBase64(), Uint8Array.fromBase64(s) |
| Manual regex escape function | RegExp.escape(input) |
| fetch('./x.json').then(r => r.json()) at build time | import x from './x.json' with { type: 'json' } |
| Eager import * as heavy from './heavy.js' | import defer * as heavy from './heavy.js' |
| try { fn() } catch ... Promise.resolve(res).then(...) | Promise.try(() => fn()).then(...) |
| for await (const x of iter) arr.push(x) | await Array.fromAsync(iter) |
development
10 high-impact React patterns and anti-patterns - state management, performance, hooks, and component design. Use when writing or reviewing React components.
development
Before writing useEffect, run through a decision tree to verify it's actually needed. Prevents the most common React anti-pattern in AI-generated code.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------