JavaScript fails by continuing. A mismatched type is coerced, a missing property is undefined, a rejected promise
nobody watched is a warning. None of it reports itself, so the discipline is to make a failure loud at the earliest
point it can be seen. Three biases decide most calls:
- Prefer the construct that refuses over the one that converts —
=== over ==, ?? over ||, Number(s) over
parseInt(s), a thrown error over a silent undefined.
- The declared engine baseline decides what may be written, not the ECMAScript edition a feature belongs to.
- Reach for the built-in globals first. A dependency earns its place by doing what
Intl, URL, structuredClone,
AbortController, and the iterator helpers do not.
Language Edition
Read the project's engine baseline before writing code: engines in package.json, browserslist, target and lib
in tsconfig.json or jsconfig.json, .nvmrc, and the CI matrix. Where a transpiler stands between source and
runtime, its target governs the output and the source may run ahead; where none does, the engine baseline's oldest
engine governs the source.
An edition number is not an availability claim. Engines ship features years before ratification and lag on others,
and the gap runs both ways inside a single edition: Array.fromAsync and Math.sumPrecise are both ES2026, yet the
first shipped in Chrome 121 and reached Baseline widely available on 2026-07-25, while the second reached Baseline newly
available on 2026-04-10 and is absent from Node.js 26. Check the feature, never the edition.
Floor edition per feature this skill's rules reference. Anything from ES2019 or earlier is available in every engine a
project can realistically target.
- ES2020 —
?., ??, globalThis, BigInt, Promise.allSettled, String.prototype.matchAll, dynamic
import(), import.meta, specified for...in order
- ES2021 —
??=, ||=, &&=, String.prototype.replaceAll, Promise.any, AggregateError, numeric separators
- ES2022 — class fields,
#private fields and methods, #x in obj, static {} blocks, top-level await,
Object.hasOwn, .at(), Error cause, RegExp d flag
- ES2023 —
toSorted, toReversed, toSpliced, with, findLast, findLastIndex
- ES2024 —
Object.groupBy, Map.groupBy, Promise.withResolvers, RegExp v flag,
String.prototype.isWellFormed
- ES2025 — iterator helpers,
Set methods (union, intersection, difference, …), Promise.try,
RegExp.escape, RegExp modifiers (?i:), duplicate named capture groups, import attributes, JSON modules
- ES2026 —
Map.prototype.getOrInsert and getOrInsertComputed, Iterator.concat, Array.fromAsync,
Error.isError, Math.sumPrecise, Uint8Array base64 and hex, JSON.rawJSON
- Stage 4, unratified —
using and await using with Symbol.dispose and DisposableStack; Temporal;
Iterator.zip. No Safari 26.x release implements any of them, so none is safe for an engine baseline that includes
Safari 26. using requires Chrome 134, Firefox 141, or Node.js 24; Temporal requires Chrome 144, Firefox 139, or
Node.js 26. Atomics.pause is the exception, reaching Chrome 133, Firefox 137, and Safari 18.4.
- Not shippable — decorators are Stage 2.7 and no engine implements them. A project using them runs TypeScript or
Babel, and TypeScript's
experimentalDecorators is the older, incompatible design.
Read [${CLAUDE_SKILL_DIR}/references/versions/es20NN.md] — one file per ratified edition, es2020.md through
es2026.md, plus [${CLAUDE_SKILL_DIR}/references/versions/stage4-queue.md] for the unratified set — when writing
against a feature near the engine baseline, and whenever raising it. Each carries what its edition added, what behavior
it changed, and the traps it introduced.
Naming
- One word per concept across the codebase.
getUser everywhere, never getUser beside fetchUserInfo and
loadCustomerRecord.
url, id, err, ctx, req, res, db, fn are the accepted abbreviations. Spell out everything else in
code. A signature written to describe an API rather than to run — Object.groupBy(items, cb) — takes the placeholder
name the documentation uses.
- No redundant context:
car.make, never car.carMake.
- File names are kebab-case:
user-service.js. Match the surrounding directory when it already differs.
- A single-letter name is legal in a scope of one or two lines — a loop index, a comparator parameter — and nowhere
else.
Declarations and Scope
- Never
var. It is function-scoped and hoists to undefined, which is why it survives only in code that predates
block scoping.
const freezes the binding, not the value. A const object is still mutable.
- One declaration per statement.
const a = 1, b = 2 breaks under a debugger and under git blame.
- Declare at first use, not at the top of the function. The distance between declaration and use is the variable's
real scope.
- A
let that is assigned exactly once in every branch wants a ternary or a helper function returning const.
Equality and Coercion
=== and !== always, with one exception: value == null tests null and undefined together, and the strict
alternative is longer without being clearer.
?? for defaults, || only when 0, "", and false genuinely mean "absent". Every configuration default is
??.
?. where the absence is expected, never to silence a bug. Data that must be present throws at the access rather
than surfacing as undefined three frames later. obj?.method() still throws when method is missing; write
obj?.method?.() when both are optional.
?? cannot be mixed with || or && without parentheses — it is a SyntaxError.
- The falsy set is
false, 0, -0, 0n, "", null, undefined, NaN. [], {}, and "0" are truthy.
Number.isNaN, never the global isNaN, which coerces first: isNaN("abc") is true.
Number(s) over parseInt(s) for a whole-string conversion. parseInt stops at the first invalid character, so
parseInt("1e3") is 1. Never pass parseInt as a bare callback — map supplies the index as the radix, and
["1","7","11"].map(parseInt) is [1, NaN, 3].
- Never compute money in a double.
(1.005).toFixed(2) is "1.00". Hold integer minor units, and format with
Intl.NumberFormat.
Object.is only for NaN and -0. Everywhere else it is === with extra ceremony.
Read [${CLAUDE_SKILL_DIR}/references/conventions/coercion.md] when a comparison behaves inconsistently between two
built-ins, when a number loses precision, or when a copy shares state it should not — it carries the four equality
algorithms and which built-in uses each, the -0 sources, BigInt mixing rules, property-order rules, and the
prototype-pollution boundary.
Functions
- Arrow functions for callbacks,
function declarations for named module-level functions. A function declaration
hoists, which lets helpers sit below the code that reads them.
- Never an arrow function as an object method or on a prototype. It has no
this of its own.
- Guard clauses first, happy path unindented. A function whose body is one
if wrapping everything wants an early
return.
- Destructure the parameter object at three or more parameters. Positional arguments past two are unreadable at the
call site and impossible to extend.
- Default parameters over
|| inside the body. They are evaluated left to right and may reference earlier
parameters.
- Rest parameters, never
arguments — arguments is array-like, absent in arrow functions, and defeats
optimization.
- Return an object when returning several values. A tuple binds callers to positional order.
- A closure keeps the whole scope alive, not the values it reads. A callback that captures one field of a large
object retains the object.
Objects and Copying
- Spread over
Object.assign for a copy, because it does not fire setters on the target. Both are shallow and both
drop the prototype.
structuredClone for a deep copy of data. It throws DataCloneError on functions and symbols, and discards the
prototype, so a class instance clones to a plain object.
Object.hasOwn(obj, key), never obj.hasOwnProperty(key) — the latter breaks on a null-prototype object.
- Dot notation for a known key, brackets only for a computed one.
Object.freeze is shallow. Freezing a tree means freezing every level, or not claiming immutability.
- Never mutate a parameter. Return a new value; the caller decides whether to keep the old one.
- Never extend a built-in prototype. It is visible to every dependency in the process.
Arrays and Iteration
for...of for side effects, array methods for transformation, for only when the index is used. forEach cannot
break and does not await; reach for it only when the callback is already a named function.
- Never
for...in on an array, and rarely on an object — it walks inherited enumerable keys.
- Always pass a comparator to
sort for numbers. The default is lexicographic: [1, 5, 10, 2].sort() is
[1, 10, 2, 5]. The comparator must return a number; (a, b) => a > b returns booleans, which makes the order
implementation-defined and the bug engine-dependent.
sort and reverse mutate. Prefer toSorted and toReversed where the engine baseline allows.
- Never
delete an array element. It leaves a hole and does not change length. Use splice or filter.
- Build a dense array with
Array.from({ length: n }) or [...Array(n)]. Array(n).map(f) calls f zero times,
because array methods skip holes while spread and for...of treat them as undefined.
Map when keys are not strings, come from outside the program, or must keep insertion order. Object keys are
stringified, and integer-like keys reorder themselves ahead of the rest.
Set for membership and deduplication — [...new Set(items)].
- Iterator helpers for a stream that is infinite, expensive, or larger than memory. They are lazy and single-pass:
draining a helper drains its source.
- Sort user-visible text with
Intl.Collator. < compares UTF-16 code units, so "ä" sorts after "z".
- Count characters with
Intl.Segmenter, not .length. .length counts code units and [...str] counts code
points; neither is a user-perceived character.
Read [${CLAUDE_SKILL_DIR}/references/conventions/iteration.md] when a loop produces the wrong count, when an array
method skips elements, or when a generator's cleanup does not run — it carries the full hole-behavior split, the
iterator-closing rules, and the Map/Set mutation-during-iteration semantics.
Classes
- Reach for a class when a type has invariants, several methods over shared state, or a lifecycle. A class with one
method and no state is a function.
#private fields, never a leading underscore. #x in obj is the only reliable same-class test; instanceof is
forgeable and fails across realms.
- Never call an overridable method from a constructor. The base constructor completes before any derived field
initializer runs, so the method sees
undefined.
- A method belongs on the prototype; an arrow-function field is an own property per instance. Use the field form
only where the function is detached from its receiver.
- Composition over
extends. Inherit only for a real "is-a" with a shared contract.
- Omit a constructor that only calls
super(...args).
- Never rely on a built-in method to return your subclass. Array methods honor
Symbol.species and the Set
methods do not; structuredClone discards the prototype either way.
Read [${CLAUDE_SKILL_DIR}/references/conventions/classes.md] when a field reads undefined, when instanceof gives
the wrong answer, or when designing a class hierarchy — it carries the full initialization order, the private-field
semantics, and the prototype-pollution rules.
Async
- Every promise is awaited or has a handler.
send().catch(reportError) for fire-and-forget, which is the one place
a .then belongs. A floating promise is a failure nobody will see.
.map(async …) needs a Promise.all around it, and forEach(async …) is always wrong — forEach discards the
returned promise and finishes before any callback body does.
- Start independent work together, await together:
const [a, b] = await Promise.all([f(), g()]). Two sequential
awaits with no dependency between them double the latency.
- Bound a large fan-out.
Promise.all over ten thousand items opens ten thousand operations.
- Pick the combinator by failure mode —
all when every result is required, allSettled when every outcome must be
observed, any for the first success, race only with an AbortController, because the losers keep running.
return await inside a try, plain return outside it. return promise inside a try returns before the
promise settles, so the catch never fires.
- Never
return, break, or continue out of a finally — it discards a pending throw.
AbortController is the cancellation protocol. Thread { signal } through; use AbortSignal.timeout(ms) for a
deadline and AbortSignal.any([...]) to combine. Distinguish an abort from a real failure before retrying.
new Promise(...) only to wrap a callback API. Where a resolver must escape, Promise.withResolvers() returns
{ promise, resolve, reject }.
- Sequential
await in a loop is correct when each step depends on the last and a defect otherwise.
using and await using release a resource on every exit path where the engine baseline allows them;
try/finally with one nested block per resource is the equivalent elsewhere.
Read [${CLAUDE_SKILL_DIR}/references/conventions/async.md] when ordering between promises and timers matters, when a
rejection surfaces in the wrong place, or when cancellation has to propagate — it carries microtask ordering, the tick
cost of await, the floating-promise catalog, and the abort-reason semantics.
Modules
- An import is a live read-only binding, not a copy. The importer sees a reassignment by the exporter and cannot
write to the binding itself. Code ported from
require often depends on the copy without knowing it.
- Never export a mutable
let. Export a function that returns the value.
- Named exports over default. A default export takes a different name in every importer, so a typo becomes
undefined rather than an error. Accept one only where a framework requires it.
- Include the file extension in every relative specifier —
"./user.js". Extensionless and directory imports are
bundler conventions, not module semantics.
- Imports at the top, grouped by origin — built-in, external, internal — even though the language hoists them
regardless of position.
- No wildcard re-exports and no barrel files inside a package. Both hide the export set and defeat tree shaking. A
package's single public entry point declared through
exports is the exception.
- Break a cycle rather than deferring the read. A cyclic binding read at module top level throws
ReferenceError;
reading it inside a function hides the cycle instead of removing it.
- Import attributes are required, not advisory:
import data from "./d.json" with { type: "json" }.
import() for genuinely conditional loading — a route, a heavy optional dependency, a flagged feature. Keep the
specifier a literal so tooling can find it.
- A side-effect import needs a comment saying why.
Read [${CLAUDE_SKILL_DIR}/references/conventions/modules.md] when a binding is undefined at module top level, when a
module appears to run twice, or when converting between CommonJS and ESM — it carries live-binding semantics, the
namespace object, evaluation order, and cycle resolution.
Errors
- Handle an error once: recover from it, or let it propagate. A
catch that logs and rethrows reports one failure
at every frame.
- Add context the underlying error does not carry, with
cause: new Error("load config", { cause: err }). Never
concatenate the inner message into the outer one.
- Subclass
Error and set this.name when a caller must branch on the failure. Match on the class, never on the
message text.
Error.isError(v) over v instanceof Error where the engine baseline allows: instanceof is false across
realms and true for a forged prototype.
- An empty
catch and a catch that only logs are the same defect.
- Let a programmer error crash. A
TypeError from a bug is not something to recover from at the call site.
Regular Expressions
- Never share a
g or y regex across calls. lastIndex persists, so the same test alternates true and false.
Build it inside the function, or drop g when a boolean is all you need.
- Named capture groups over positional.
m.groups.year survives inserting a group; $1 does not.
matchAll requires g and throws TypeError without it. replaceAll throws on a regex without g.
- Use the
v flag for anything matching user text. It is a stricter superset of u: it adds set operations and
properties of strings, and it requires escaping characters u allows raw, so the switch is not mechanical.
RegExp.escape before interpolating data into a pattern. An unescaped ( from user input is a SyntaxError and
an unescaped (a+)+ is a denial of service.
- Nested unbounded quantifiers over overlapping sets are a security bug, not a performance one. A pattern applied to
external input needs bounded quantifiers or a parser.
- A regex cannot parse a nested structure. Reach for
URL, Intl.Segmenter, or a real parser.
Read [${CLAUDE_SKILL_DIR}/references/conventions/regexp.md] when a pattern matches inconsistently across calls or when
migrating a pattern from u to v — it carries the statefulness rules, the v differences, and the backtracking
shapes.
Intl and Built-in Globals
- Never compare an
Intl result to a literal and never parse one back. Output is permitted to differ between
engines and CLDR releases, and it contains non-breaking and narrow-no-break spaces that are invisible in a diff.
Assert with formatToParts, or snapshot.
- Pin the locale in a test. An unqualified formatter follows the host default.
- Hoist a formatter out of a loop. Construction searches the locale database;
format does not.
Intl.ListFormat for joining a list, Intl.PluralRules for plural selection, Intl.Collator for sorting.
arr.join(", ") and n === 1 ? "x" : "xs" are English-only and wrong even there.
URL and URLSearchParams for anything URL-shaped. String concatenation gets encoding wrong.
crypto.randomUUID and crypto.getRandomValues where a value must be unpredictable. Math.random is never
suitable for a token or an identifier.
Read [${CLAUDE_SKILL_DIR}/references/conventions/intl.md] when formatting anything a user reads, or when a formatted
string has to be tested — it carries the per-object routing, the reuse rules, and the built-in globals worth reaching
for.
JSDoc and Documentation
- JSDoc types are checked only with
// @ts-check or checkJs. Without one, the annotations are decoration.
- Annotate the module boundary and what inference cannot reach — exported symbols, an empty literal filled later, a
value parsed from JSON. Never restate a type the checker already infers.
{ b?: number } for an optional property, not { b: number= }, which is only valid on a @param.
- Write
{number | null}, not {?number}. Closure nullability syntax is legacy; Object and object degrade to
any.
- A doc comment on an exported symbol is API documentation and is expected even where code comments are not.
Describe the contract, never the implementation, and update it in the edit that changes the signature.
Read [${CLAUDE_SKILL_DIR}/references/conventions/jsdoc.md] when typing a plain-JavaScript project or when a JSDoc type
is silently ignored — it carries the supported tag set, @import and @satisfies, and the syntax that does not work.
Style Guides
The two style guides most often cited for JavaScript are frozen. Citing either as authority produces outdated advice.
- The Google JavaScript Style Guide opens with "This guide is no longer being updated. Google recommends migrating to
TypeScript." It targets ES6, mentions
goog.module sixty-two times, requires all fields to be declared in the
constructor, and contains no occurrence of #private, optional chaining, or nullish coalescing.
eslint-config-airbnb last released 19.0.4 in December 2021, and the last substantive change to the shared config
was July 2022. Its rules predate class fields being universally available, iterator helpers, Set methods, .at(),
structuredClone, and Object.groupBy.
Follow the rules in this skill and the project's own lint configuration. When a project's ESLint config extends one of
these, follow the project and say once that the base config is frozen.
Code Navigation
typescript-language-server is configured for .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, and .cts. Use LSP
tools rather than Grep or Glob for anything that is a JavaScript identifier — they resolve module specifiers,
re-exports, aliases, and inferred types, which text search cannot.
- Where a symbol is defined —
goToDefinition
- Every use of a symbol —
findReferences
- Type, signature, or doc of a symbol —
hover
- Symbols in one file —
documentSymbol; across the project — workspaceSymbol
- Types implementing an interface —
goToImplementation
- Call graph in either direction —
incomingCalls, outgoingCalls
Grep and Glob stay correct for comments, string literals, log messages, environment variable names, CSS class names,
config values, and file-name patterns. Subagents exploring JavaScript reach the same LSP server — instruct them to use
it.
Application
When writing JavaScript, apply these conventions silently — do not narrate a rule while following it. Where existing
code contradicts one, follow the codebase and flag the divergence once.
When reviewing JavaScript, cite the violation and show the fix inline. Do not lecture.
Bad: "Best practice is to use nullish coalescing rather than logical OR for default values."
Good: options.retries || 3 -> options.retries ?? 3 — 0 is a valid retry count
Integration
The coding skill governs workflow — discovery, decomposition, verification. This skill governs JavaScript
implementation choices and wins on any question of how JavaScript reads. Both are active at once.
The typescript skill extends this one and does not restate it. The nodejs and bun skills own their runtimes,
and the split with this skill is the same in both directions: this skill owns module syntax and live-binding semantics,
they own resolution; this skill owns the cancellation protocol and the globals that behave the same everywhere, they own
which of their own APIs accept a signal and how their own fetch behaves. The vitest skill owns test authoring in
Vitest, and each runtime skill owns the test runner it ships. Browser platform and framework concerns belong to the
frontend plugin.
When in doubt, make the failure loud.
1---2name: javascript3description: Write and review JavaScript: declarations, equality and coercion, functions, objects, iteration, classes, async and promise semantics, ES modules, regular expressions, JSDoc typing, and the ECMAScript features a project's engine baseline permits.4---56**JavaScript fails by continuing.** A mismatched type is coerced, a missing property is `undefined`, a rejected promise7nobody watched is a warning. None of it reports itself, so the discipline is to make a failure loud at the earliest8point it can be seen. Three biases decide most calls:910- **Prefer the construct that refuses over the one that converts** — `===` over `==`, `??` over `||`, `Number(s)` over11 `parseInt(s)`, a thrown error over a silent `undefined`.12- **The declared engine baseline decides what may be written**, not the ECMAScript edition a feature belongs to.13- **Reach for the built-in globals first.** A dependency earns its place by doing what `Intl`, `URL`, `structuredClone`,14 `AbortController`, and the iterator helpers do not.1516## Language Edition1718Read the project's engine baseline before writing code: `engines` in `package.json`, `browserslist`, `target` and `lib`19in `tsconfig.json` or `jsconfig.json`, `.nvmrc`, and the CI matrix. Where a transpiler stands between source and20runtime, its target governs the output and the source may run ahead; where none does, the engine baseline's oldest21engine governs the source.2223**An edition number is not an availability claim.** Engines ship features years before ratification and lag on others,24and the gap runs both ways inside a single edition: `Array.fromAsync` and `Math.sumPrecise` are both ES2026, yet the25first shipped in Chrome 121 and reached Baseline widely available on 2026-07-25, while the second reached Baseline newly26available on 2026-04-10 and is absent from Node.js 26. Check the feature, never the edition.2728Floor edition per feature this skill's rules reference. Anything from ES2019 or earlier is available in every engine a29project can realistically target.3031- **ES2020** — `?.`, `??`, `globalThis`, `BigInt`, `Promise.allSettled`, `String.prototype.matchAll`, dynamic32 `import()`, `import.meta`, specified `for...in` order33- **ES2021** — `??=`, `||=`, `&&=`, `String.prototype.replaceAll`, `Promise.any`, `AggregateError`, numeric separators34- **ES2022** — class fields, `#private` fields and methods, `#x in obj`, `static {}` blocks, top-level `await`,35 `Object.hasOwn`, `.at()`, `Error` `cause`, RegExp `d` flag36- **ES2023** — `toSorted`, `toReversed`, `toSpliced`, `with`, `findLast`, `findLastIndex`37- **ES2024** — `Object.groupBy`, `Map.groupBy`, `Promise.withResolvers`, RegExp `v` flag,38 `String.prototype.isWellFormed`39- **ES2025** — iterator helpers, `Set` methods (`union`, `intersection`, `difference`, …), `Promise.try`,40 `RegExp.escape`, RegExp modifiers `(?i:)`, duplicate named capture groups, import attributes, JSON modules41- **ES2026** — `Map.prototype.getOrInsert` and `getOrInsertComputed`, `Iterator.concat`, `Array.fromAsync`,42 `Error.isError`, `Math.sumPrecise`, `Uint8Array` base64 and hex, `JSON.rawJSON`43- **Stage 4, unratified** — `using` and `await using` with `Symbol.dispose` and `DisposableStack`; `Temporal`;44 `Iterator.zip`. No Safari 26.x release implements any of them, so none is safe for an engine baseline that includes45 Safari 26. `using` requires Chrome 134, Firefox 141, or Node.js 24; `Temporal` requires Chrome 144, Firefox 139, or46 Node.js 26. `Atomics.pause` is the exception, reaching Chrome 133, Firefox 137, and Safari 18.4.47- **Not shippable** — decorators are Stage 2.7 and no engine implements them. A project using them runs TypeScript or48 Babel, and TypeScript's `experimentalDecorators` is the older, incompatible design.4950Read [`${CLAUDE_SKILL_DIR}/references/versions/es20NN.md`] — one file per ratified edition, `es2020.md` through51`es2026.md`, plus [`${CLAUDE_SKILL_DIR}/references/versions/stage4-queue.md`] for the unratified set — when writing52against a feature near the engine baseline, and whenever raising it. Each carries what its edition added, what behavior53it changed, and the traps it introduced.5455## Naming5657- **One word per concept across the codebase.** `getUser` everywhere, never `getUser` beside `fetchUserInfo` and58 `loadCustomerRecord`.59- **`url`, `id`, `err`, `ctx`, `req`, `res`, `db`, `fn` are the accepted abbreviations.** Spell out everything else in60 code. A signature written to describe an API rather than to run — `Object.groupBy(items, cb)` — takes the placeholder61 name the documentation uses.62- **No redundant context**: `car.make`, never `car.carMake`.63- **File names are kebab-case**: `user-service.js`. Match the surrounding directory when it already differs.64- **A single-letter name is legal in a scope of one or two lines** — a loop index, a comparator parameter — and nowhere65 else.6667## Declarations and Scope6869- **Never `var`.** It is function-scoped and hoists to `undefined`, which is why it survives only in code that predates70 block scoping.71- **`const` freezes the binding, not the value.** A `const` object is still mutable.72- **One declaration per statement.** `const a = 1, b = 2` breaks under a debugger and under `git blame`.73- **Declare at first use, not at the top of the function.** The distance between declaration and use is the variable's74 real scope.75- **A `let` that is assigned exactly once in every branch wants a ternary or a helper function returning `const`.**7677## Equality and Coercion7879- **`===` and `!==` always, with one exception:** `value == null` tests `null` and `undefined` together, and the strict80 alternative is longer without being clearer.81- **`??` for defaults, `||` only when `0`, `""`, and `false` genuinely mean "absent".** Every configuration default is82 `??`.83- **`?.` where the absence is expected, never to silence a bug.** Data that must be present throws at the access rather84 than surfacing as `undefined` three frames later. `obj?.method()` still throws when `method` is missing; write85 `obj?.method?.()` when both are optional.86- **`??` cannot be mixed with `||` or `&&` without parentheses** — it is a `SyntaxError`.87- **The falsy set is `false`, `0`, `-0`, `0n`, `""`, `null`, `undefined`, `NaN`.** `[]`, `{}`, and `"0"` are truthy.88- **`Number.isNaN`, never the global `isNaN`**, which coerces first: `isNaN("abc")` is `true`.89- **`Number(s)` over `parseInt(s)` for a whole-string conversion.** `parseInt` stops at the first invalid character, so90 `parseInt("1e3")` is `1`. Never pass `parseInt` as a bare callback — `map` supplies the index as the radix, and91 `["1","7","11"].map(parseInt)` is `[1, NaN, 3]`.92- **Never compute money in a double.** `(1.005).toFixed(2)` is `"1.00"`. Hold integer minor units, and format with93 `Intl.NumberFormat`.94- **`Object.is` only for `NaN` and `-0`.** Everywhere else it is `===` with extra ceremony.9596Read [`${CLAUDE_SKILL_DIR}/references/conventions/coercion.md`] when a comparison behaves inconsistently between two97built-ins, when a number loses precision, or when a copy shares state it should not — it carries the four equality98algorithms and which built-in uses each, the `-0` sources, BigInt mixing rules, property-order rules, and the99prototype-pollution boundary.100101## Functions102103- **Arrow functions for callbacks, `function` declarations for named module-level functions.** A `function` declaration104 hoists, which lets helpers sit below the code that reads them.105- **Never an arrow function as an object method or on a prototype.** It has no `this` of its own.106- **Guard clauses first, happy path unindented.** A function whose body is one `if` wrapping everything wants an early107 return.108- **Destructure the parameter object at three or more parameters.** Positional arguments past two are unreadable at the109 call site and impossible to extend.110- **Default parameters over `||` inside the body.** They are evaluated left to right and may reference earlier111 parameters.112- **Rest parameters, never `arguments`** — `arguments` is array-like, absent in arrow functions, and defeats113 optimization.114- **Return an object when returning several values.** A tuple binds callers to positional order.115- **A closure keeps the whole scope alive, not the values it reads.** A callback that captures one field of a large116 object retains the object.117118## Objects and Copying119120- **Spread over `Object.assign` for a copy**, because it does not fire setters on the target. Both are shallow and both121 drop the prototype.122- **`structuredClone` for a deep copy of data.** It throws `DataCloneError` on functions and symbols, and discards the123 prototype, so a class instance clones to a plain object.124- **`Object.hasOwn(obj, key)`, never `obj.hasOwnProperty(key)`** — the latter breaks on a null-prototype object.125- **Dot notation for a known key, brackets only for a computed one.**126- **`Object.freeze` is shallow.** Freezing a tree means freezing every level, or not claiming immutability.127- **Never mutate a parameter.** Return a new value; the caller decides whether to keep the old one.128- **Never extend a built-in prototype.** It is visible to every dependency in the process.129130## Arrays and Iteration131132- **`for...of` for side effects, array methods for transformation, `for` only when the index is used.** `forEach` cannot133 `break` and does not await; reach for it only when the callback is already a named function.134- **Never `for...in` on an array**, and rarely on an object — it walks inherited enumerable keys.135- **Always pass a comparator to `sort` for numbers.** The default is lexicographic: `[1, 5, 10, 2].sort()` is136 `[1, 10, 2, 5]`. The comparator must return a number; `(a, b) => a > b` returns booleans, which makes the order137 implementation-defined and the bug engine-dependent.138- **`sort` and `reverse` mutate.** Prefer `toSorted` and `toReversed` where the engine baseline allows.139- **Never `delete` an array element.** It leaves a hole and does not change `length`. Use `splice` or `filter`.140- **Build a dense array with `Array.from({ length: n })` or `[...Array(n)]`.** `Array(n).map(f)` calls `f` zero times,141 because array methods skip holes while spread and `for...of` treat them as `undefined`.142- **`Map` when keys are not strings, come from outside the program, or must keep insertion order.** Object keys are143 stringified, and integer-like keys reorder themselves ahead of the rest.144- **`Set` for membership and deduplication** — `[...new Set(items)]`.145- **Iterator helpers for a stream that is infinite, expensive, or larger than memory.** They are lazy and single-pass:146 draining a helper drains its source.147- **Sort user-visible text with `Intl.Collator`.** `<` compares UTF-16 code units, so `"ä"` sorts after `"z"`.148- **Count characters with `Intl.Segmenter`, not `.length`.** `.length` counts code units and `[...str]` counts code149 points; neither is a user-perceived character.150151Read [`${CLAUDE_SKILL_DIR}/references/conventions/iteration.md`] when a loop produces the wrong count, when an array152method skips elements, or when a generator's cleanup does not run — it carries the full hole-behavior split, the153iterator-closing rules, and the `Map`/`Set` mutation-during-iteration semantics.154155## Classes156157- **Reach for a class when a type has invariants, several methods over shared state, or a lifecycle.** A class with one158 method and no state is a function.159- **`#private` fields, never a leading underscore.** `#x in obj` is the only reliable same-class test; `instanceof` is160 forgeable and fails across realms.161- **Never call an overridable method from a constructor.** The base constructor completes before any derived field162 initializer runs, so the method sees `undefined`.163- **A method belongs on the prototype; an arrow-function field is an own property per instance.** Use the field form164 only where the function is detached from its receiver.165- **Composition over `extends`.** Inherit only for a real "is-a" with a shared contract.166- **Omit a constructor that only calls `super(...args)`.**167- **Never rely on a built-in method to return your subclass.** Array methods honor `Symbol.species` and the `Set`168 methods do not; `structuredClone` discards the prototype either way.169170Read [`${CLAUDE_SKILL_DIR}/references/conventions/classes.md`] when a field reads `undefined`, when `instanceof` gives171the wrong answer, or when designing a class hierarchy — it carries the full initialization order, the private-field172semantics, and the prototype-pollution rules.173174## Async175176- **Every promise is awaited or has a handler.** `send().catch(reportError)` for fire-and-forget, which is the one place177 a `.then` belongs. A floating promise is a failure nobody will see.178- **`.map(async …)` needs a `Promise.all` around it, and `forEach(async …)` is always wrong** — `forEach` discards the179 returned promise and finishes before any callback body does.180- **Start independent work together, await together**: `const [a, b] = await Promise.all([f(), g()])`. Two sequential181 awaits with no dependency between them double the latency.182- **Bound a large fan-out.** `Promise.all` over ten thousand items opens ten thousand operations.183- **Pick the combinator by failure mode** — `all` when every result is required, `allSettled` when every outcome must be184 observed, `any` for the first success, `race` only with an `AbortController`, because the losers keep running.185- **`return await` inside a `try`, plain `return` outside it.** `return promise` inside a `try` returns before the186 promise settles, so the `catch` never fires.187- **Never `return`, `break`, or `continue` out of a `finally`** — it discards a pending throw.188- **`AbortController` is the cancellation protocol.** Thread `{ signal }` through; use `AbortSignal.timeout(ms)` for a189 deadline and `AbortSignal.any([...])` to combine. Distinguish an abort from a real failure before retrying.190- **`new Promise(...)` only to wrap a callback API.** Where a resolver must escape, `Promise.withResolvers()` returns191 `{ promise, resolve, reject }`.192- **Sequential `await` in a loop is correct when each step depends on the last** and a defect otherwise.193- **`using` and `await using` release a resource on every exit path** where the engine baseline allows them;194 `try`/`finally` with one nested block per resource is the equivalent elsewhere.195196Read [`${CLAUDE_SKILL_DIR}/references/conventions/async.md`] when ordering between promises and timers matters, when a197rejection surfaces in the wrong place, or when cancellation has to propagate — it carries microtask ordering, the tick198cost of `await`, the floating-promise catalog, and the abort-reason semantics.199200## Modules201202- **An import is a live read-only binding, not a copy.** The importer sees a reassignment by the exporter and cannot203 write to the binding itself. Code ported from `require` often depends on the copy without knowing it.204- **Never export a mutable `let`.** Export a function that returns the value.205- **Named exports over default.** A default export takes a different name in every importer, so a typo becomes206 `undefined` rather than an error. Accept one only where a framework requires it.207- **Include the file extension in every relative specifier** — `"./user.js"`. Extensionless and directory imports are208 bundler conventions, not module semantics.209- **Imports at the top, grouped by origin** — built-in, external, internal — even though the language hoists them210 regardless of position.211- **No wildcard re-exports and no barrel files inside a package.** Both hide the export set and defeat tree shaking. A212 package's single public entry point declared through `exports` is the exception.213- **Break a cycle rather than deferring the read.** A cyclic binding read at module top level throws `ReferenceError`;214 reading it inside a function hides the cycle instead of removing it.215- **Import attributes are required, not advisory**: `import data from "./d.json" with { type: "json" }`.216- **`import()` for genuinely conditional loading** — a route, a heavy optional dependency, a flagged feature. Keep the217 specifier a literal so tooling can find it.218- **A side-effect import needs a comment saying why.**219220Read [`${CLAUDE_SKILL_DIR}/references/conventions/modules.md`] when a binding is `undefined` at module top level, when a221module appears to run twice, or when converting between CommonJS and ESM — it carries live-binding semantics, the222namespace object, evaluation order, and cycle resolution.223224## Errors225226- **Handle an error once: recover from it, or let it propagate.** A `catch` that logs and rethrows reports one failure227 at every frame.228- **Add context the underlying error does not carry, with `cause`**: `new Error("load config", { cause: err })`. Never229 concatenate the inner message into the outer one.230- **Subclass `Error` and set `this.name` when a caller must branch on the failure.** Match on the class, never on the231 message text.232- **`Error.isError(v)` over `v instanceof Error`** where the engine baseline allows: `instanceof` is `false` across233 realms and `true` for a forged prototype.234- **An empty `catch` and a `catch` that only logs are the same defect.**235- **Let a programmer error crash.** A `TypeError` from a bug is not something to recover from at the call site.236237## Regular Expressions238239- **Never share a `g` or `y` regex across calls.** `lastIndex` persists, so the same test alternates true and false.240 Build it inside the function, or drop `g` when a boolean is all you need.241- **Named capture groups over positional.** `m.groups.year` survives inserting a group; `$1` does not.242- **`matchAll` requires `g` and throws `TypeError` without it.** `replaceAll` throws on a regex without `g`.243- **Use the `v` flag for anything matching user text.** It is a stricter superset of `u`: it adds set operations and244 properties of strings, and it requires escaping characters `u` allows raw, so the switch is not mechanical.245- **`RegExp.escape` before interpolating data into a pattern.** An unescaped `(` from user input is a `SyntaxError` and246 an unescaped `(a+)+` is a denial of service.247- **Nested unbounded quantifiers over overlapping sets are a security bug**, not a performance one. A pattern applied to248 external input needs bounded quantifiers or a parser.249- **A regex cannot parse a nested structure.** Reach for `URL`, `Intl.Segmenter`, or a real parser.250251Read [`${CLAUDE_SKILL_DIR}/references/conventions/regexp.md`] when a pattern matches inconsistently across calls or when252migrating a pattern from `u` to `v` — it carries the statefulness rules, the `v` differences, and the backtracking253shapes.254255## Intl and Built-in Globals256257- **Never compare an `Intl` result to a literal and never parse one back.** Output is permitted to differ between258 engines and CLDR releases, and it contains non-breaking and narrow-no-break spaces that are invisible in a diff.259 Assert with `formatToParts`, or snapshot.260- **Pin the locale in a test.** An unqualified formatter follows the host default.261- **Hoist a formatter out of a loop.** Construction searches the locale database; `format` does not.262- **`Intl.ListFormat` for joining a list, `Intl.PluralRules` for plural selection, `Intl.Collator` for sorting.**263 `arr.join(", ")` and `n === 1 ? "x" : "xs"` are English-only and wrong even there.264- **`URL` and `URLSearchParams` for anything URL-shaped.** String concatenation gets encoding wrong.265- **`crypto.randomUUID` and `crypto.getRandomValues` where a value must be unpredictable.** `Math.random` is never266 suitable for a token or an identifier.267268Read [`${CLAUDE_SKILL_DIR}/references/conventions/intl.md`] when formatting anything a user reads, or when a formatted269string has to be tested — it carries the per-object routing, the reuse rules, and the built-in globals worth reaching270for.271272## JSDoc and Documentation273274- **JSDoc types are checked only with `// @ts-check` or `checkJs`.** Without one, the annotations are decoration.275- **Annotate the module boundary and what inference cannot reach** — exported symbols, an empty literal filled later, a276 value parsed from JSON. Never restate a type the checker already infers.277- **`{ b?: number }` for an optional property**, not `{ b: number= }`, which is only valid on a `@param`.278- **Write `{number | null}`, not `{?number}`.** Closure nullability syntax is legacy; `Object` and `object` degrade to279 `any`.280- **A doc comment on an exported symbol is API documentation** and is expected even where code comments are not.281 Describe the contract, never the implementation, and update it in the edit that changes the signature.282283Read [`${CLAUDE_SKILL_DIR}/references/conventions/jsdoc.md`] when typing a plain-JavaScript project or when a JSDoc type284is silently ignored — it carries the supported tag set, `@import` and `@satisfies`, and the syntax that does not work.285286## Style Guides287288The two style guides most often cited for JavaScript are frozen. Citing either as authority produces outdated advice.289290- **The Google JavaScript Style Guide opens with "This guide is no longer being updated. Google recommends migrating to291 TypeScript."** It targets ES6, mentions `goog.module` sixty-two times, requires all fields to be declared in the292 constructor, and contains no occurrence of `#private`, optional chaining, or nullish coalescing.293- **`eslint-config-airbnb` last released 19.0.4 in December 2021**, and the last substantive change to the shared config294 was July 2022. Its rules predate class fields being universally available, iterator helpers, `Set` methods, `.at()`,295 `structuredClone`, and `Object.groupBy`.296297Follow the rules in this skill and the project's own lint configuration. When a project's ESLint config extends one of298these, follow the project and say once that the base config is frozen.299300## Code Navigation301302`typescript-language-server` is configured for `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, and `.cts`. Use LSP303tools rather than Grep or Glob for anything that is a JavaScript identifier — they resolve module specifiers,304re-exports, aliases, and inferred types, which text search cannot.305306- **Where a symbol is defined** — `goToDefinition`307- **Every use of a symbol** — `findReferences`308- **Type, signature, or doc of a symbol** — `hover`309- **Symbols in one file** — `documentSymbol`; **across the project** — `workspaceSymbol`310- **Types implementing an interface** — `goToImplementation`311- **Call graph in either direction** — `incomingCalls`, `outgoingCalls`312313Grep and Glob stay correct for comments, string literals, log messages, environment variable names, CSS class names,314config values, and file-name patterns. Subagents exploring JavaScript reach the same LSP server — instruct them to use315it.316317## Application318319When **writing** JavaScript, apply these conventions silently — do not narrate a rule while following it. Where existing320code contradicts one, follow the codebase and flag the divergence once.321322When **reviewing** JavaScript, cite the violation and show the fix inline. Do not lecture.323324```325Bad: "Best practice is to use nullish coalescing rather than logical OR for default values."326Good: options.retries || 3 -> options.retries ?? 3 — 0 is a valid retry count327```328329## Integration330331The **coding** skill governs workflow — discovery, decomposition, verification. This skill governs JavaScript332implementation choices and wins on any question of how JavaScript reads. Both are active at once.333334The **typescript** skill extends this one and does not restate it. The **nodejs** and **bun** skills own their runtimes,335and the split with this skill is the same in both directions: this skill owns module syntax and live-binding semantics,336they own resolution; this skill owns the cancellation protocol and the globals that behave the same everywhere, they own337which of their own APIs accept a signal and how their own `fetch` behaves. The **vitest** skill owns test authoring in338Vitest, and each runtime skill owns the test runner it ships. Browser platform and framework concerns belong to the339frontend plugin.340341**When in doubt, make the failure loud.**