Install
openclaw skills install @ivangdavila/javascriptWrites, debugs, and reviews JavaScript: async and the event loop, coercion, closures, dates, Unicode, regex, and modern ES2023+ APIs. Use when JS throws TypeError or "undefined is not a function", a promise never settles or a rejection goes unhandled, NaN or [object Object] appears, dates shift by a day, sorting or equality misbehaves, memory grows, a regex hangs, JSON loses precision, a Node process won't exit or ignores signals, or fetch doesn't reject on a 404; also when choosing data structures, handling errors, profiling slow code, or checking whether a feature is safe for a target Node or browser version. Covers Node and browser runtime edges. Not for TypeScript type-system design or framework internals.
openclaw skills install @ivangdavila/javascriptUser preferences and memory live in ~/Clawic/data/javascript/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/javascript/ or ~/clawic/javascript/), move it to ~/Clawic/data/javascript/.
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/javascript/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| runtime_target | node | browser | both | node | Selects which platform file applies by default (node.md vs browser.md) and which floor gates advice |
| node_floor | number (Node major) | from package.json engines, else 22 | Gates every recommendation against the feature-floor table in modern.md; flags any API above the floor |
| module_system | esm | cjs | dual | esm | Switches module guidance and import/export examples in modern.md; dual activates the dual-package hazard checks |
| browser_floor | text (e.g. "Safari 16+", "last 2 years") | none | When set, gates syntax and API advice for browser-targeted code the way node_floor gates Node |
Preference areas to record as the user reveals them:
var, ==, callback APIs) in review vs only on request| Situation | Go to |
|---|---|
| TypeError/ReferenceError, NaN appearing, value undefined after await, works-in-dev-only, heisenbug | debug.md |
| Promise/await bug, rejection handling, cancellation, concurrency limits, races | async.md |
| Throwing, catching, custom errors, cause chains, global error hooks, serializing errors | errors.md |
== surprise, truthiness, implicit conversion, ?? vs || | coercion.md |
| Array/Object/Map/Set choice, copying, sorting, iteration traps | collections.md |
| ES2020+ syntax semantics, feature floors, modules (ESM/CJS), classes, generators/iterators | modern.md |
| Memory grows, listener/timer leaks, WeakMap/WeakRef, heap snapshots | memory-leaks.md |
| Slow code, jank, benchmarks, GC pressure, workers | performance.md |
Regex wrong matches, stateful lastIndex, catastrophic backtracking, Unicode flags | regex.md |
| JSON precision loss, Date/Map round-trips, reviver/replacer, canonicalization | json.md |
| Env vars, process exit, signals, streams, Buffer, child processes | node.md |
| DOM events, storage, script loading, fetch response handling | browser.md |
Anything else (numbers, dates, strings, this, timers) | Sections below |
=== always; the only defensible == is the idiom x == null — it matches both null and undefined in one check. Never expand it to two comparisons.===: Math.abs(a - b) <= Number.EPSILON * Math.max(1, Math.abs(a), Math.abs(b)). Worked: 0.1 + 0.2 - 0.3 ≈ 5.6e-17, inside the band (EPSILON ≈ 2.2e-16). Money never touches floats: integer minor units.unhandledrejection in browsers.toSorted/toReversed/with and spread; structuredClone only when depth is real. Runtime floors for all of these: modern.md.performance.now() (monotonic); wall-clock timestamps from Date.now(). Never subtract two Date.now() calls for benchmarks — NTP can step it backwards mid-measurement.for...of + await = sequential; Promise.all(arr.map(f)) = parallel, fail-fast, and it does NOT cancel the losers..length counts UTF-16 code units, not characters: "😀".length === 2. Slice user-visible text with Intl.Segmenter, never by index.modern.md before shipping ES2023+ APIs — one toSorted call breaks Node 18 at runtime, not at build time.Number.MAX_SAFE_INTEGER = 2^53 − 1 = 9007199254740991 (16 digits). Integer ids with ≥16 digits can silently round in JSON.parse — transport ids as strings; use BigInt only for arithmetic (JSON.stringify on BigInt throws).(1.005).toFixed(2) === "1.00" — binary representation, not a rounding bug you can fix locally. Compute in integer cents; display through Intl.NumberFormat.Number("12px") → NaN (whole-string strict), parseInt("12px") → 12 (prefix). Number("") and Number(null) → 0; Number(undefined) → NaN.["10","10","10"].map(parseInt) → [10, NaN, 2]: map passes the index as radix. Always wrap: map(s => parseInt(s, 10)).-0 exists: Object.is(0, -0) is false, 1 / -0 === -Infinity. It appears when rounding negatives toward zero and survives into keys and comparisons.new Date(2025, 0, 31) is Jan 31."2024-01-01") → UTC midnight; date-time without offset ("2024-01-01T00:00") → local time. Same input day can render one day off in negative-offset zones. Any non-ISO string is implementation-defined — never parse it.d = new Date(2025, 0, 31); d.setMonth(1) → March 3 (Feb 31 normalizes). Pin the day to 1 before month arithmetic, then restore a clamped day.getTimezoneOffset() is UTC minus local: UTC+2 reports -120. Sign errors here produce double-offset bugs that cancel out in your own timezone and explode in others.Date.now()) plus IANA zone name; format only at the edge with Intl.DateTimeFormat.length, slice, charAt operate on UTF-16 units: "👨👩👧".length === 8. [...str] yields code points; user-perceived characters need Intl.Segmenter. Index-based slicing can cut a surrogate pair → U+FFFD garbage."é" !== "e" + combining accent even when rendered identically — str.normalize("NFC") both sides.(a, b) => a.localeCompare(b, undefined, {numeric: true}) → ["file2", "file10"], accents ordered correctly. Bare < compares code units ("Z" < "a" is true)./g and /y regexes are stateful: lastIndex persists across calls, so re.test(s) twice on the same string can alternate true/false. Drop /g for single tests or reset re.lastIndex = 0 (depth: regex.md).Object.keys({b:1, 2:2, a:3, 1:4}) → ["1","2","b","a"]. Never encode order in numeric-string keys — use Map or an array.obj[userKey] with "__proto__" pollutes the prototype. Use Map or Object.create(null) for user-keyed storage.Object.hasOwn(obj, k) over obj.hasOwnProperty(k): works on null-prototype objects and can't be shadowed.{...obj} invokes getters (snapshots values); Object.assign(target, src) fires setters on target. Same shallow result, different side effects.this: arrow = lexical (use in callbacks); regular = call-site (methods, event handlers that want the element). setTimeout(obj.method) detaches this — wrap in an arrow.setTimeout yields between runs.setTimeout beyond 5 levels → 4ms minimum; background tabs throttle to ≥1000ms (often far more). Clocks and animations must compute elapsed = Date.now() - start each tick — accumulating +interval drifts.setTimeout delay above 2147483647 ms (~24.8 days, 32-bit signed) fires almost immediately. Long schedules: persist the target time and re-arm.process.nextTick queue → promise microtasks → timers/macrotasks. setImmediate vs setTimeout(0): nondeterministic at top level, setImmediate always first inside I/O callbacks.await new Promise(r => setTimeout(r))) between slices.| Trap | Why it fails | Do instead |
|---|---|---|
forEach(async x => ...) | forEach ignores returned promises: everything runs at once, "finishes" instantly | for...of (sequential) or Promise.all(arr.map(f)) |
Array(3).fill({}) | one shared object, three references | Array.from({length: 3}, () => ({})) |
return fetchThing() inside try | the rejection settles after try exits — catch never sees it | return await fetchThing() |
Promise.race([op, timeout]) | the loser keeps running and holding sockets/memory | AbortSignal.timeout(ms) passed into the op |
return/throw inside finally | overrides the try's result and swallows its exception | finally is for cleanup only |
throw "failed" | no stack, instanceof Error false, breaks error middleware | new Error("failed", {cause: err}) |
JSON.parse(JSON.stringify(x)) as deep clone | drops undefined/functions, Date → string, Map/Set → {}, throws on cycles | structuredClone(x) |
obj.fn?.() when fn is a number | ?. guards null/undefined only, not "not callable" | typeof obj.fn === "function" && obj.fn() |
if (x) for "is x set" | silently drops 0, "", false | x != null |
setInterval around async work | next run starts while the previous still awaits — overlapping executions | chained setTimeout re-armed after each completion |
innerHTML with user text | the text executes as markup — XSS | textContent, or one sanitizer at the render boundary |
Before emitting JS code or a review verdict, verify:
return await-ed, not return-ed?modern.md table vs node_floor / browser_floor)?JSON.parse as a number?cause where context was added?sort, reverse, splice, Object.assign) intentional, not incidental?#x in obj); factories for one-off capability objects and simple DI. Field-initializer arrow functions are the worst of both (→ modern.md Classes).some/every/find. Switching styles without a measurement is churn, not optimization.More Clawic skills, get them at https://clawic.com/skills/javascript (install if the user confirms):
typescript — the type system layered on top of this languagenodejs — Node platform operations beyond the language: servers, tooling, deploymentreact — framework work where these language rules get appliedregex — pattern crafting beyond JS-specific regex behaviorPart of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/javascript.