JavaScript
Writes, 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 …
Iván
@ivangdavila
What This Skill Does
Debugs, writes, and reviews JavaScript code across Node and browser runtimes. Covers async/event loop, coercion, closures, dates, Unicode, regex, and ES2023+ APIs, with runtime-specific feature-floor gating and per-user configuration.
Replaces trial-and-error debugging and scattered MDN lookups by providing structured guides for common JS pitfalls, runtime-specific advice, and configurable feature-safety checks.
When to Use It
- Debug a TypeError, NaN, or 'undefined is not a function' in JavaScript
- Fix a promise that never settles or an unhandled rejection
- Diagnose date off-by-one errors or timezone shifts
- Choose between Map, Set, Object, or Array for a data structure
- Check if an ES2023+ feature is safe for a target Node or browser version
- Profile slow JavaScript code or diagnose memory growth and leaks
Install
$ 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/.
Configuration
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:
- tooling — package manager, bundler, test runner, lint/format stack — affects which commands and config examples are offered
- conventions — style (semicolons, functional vs imperative iteration), error-handling shape (exceptions vs result values) — affects generated code and review verdicts
- platform — Deno/Bun/edge runtimes, TypeScript presence, monorepo layout — affects which floors and module advice apply
- safety posture — how proactively to flag legacy patterns (
var,==, callback APIs) in review vs only on request
When To Use
- Debugging JavaScript: wrong values, NaN, TypeErrors, ordering surprises, async bugs, timezone shifts
- Writing or reviewing JS (Node or browser) for correctness and modern idioms
- Choosing data structures (Object/Map/Array/Set), copy semantics, iteration strategy, or error-handling shape
- Deciding whether an ES2020+ feature is safe for the target runtime
- Diagnosing memory growth, leaks, slow code, or an unresponsive event loop
- Not for TypeScript type-system design or framework internals — this is the core language
Quick Reference
| 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 |
Core Rules
===always; the only defensible==is the idiomx == null— it matches both null and undefined in one check. Never expand it to two comparisons.- Float equality is a band, not
===: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. - Every promise gets a handler attached synchronously — a rejection with no handler when the microtask queue drains crashes Node (default since Node >=15) and fires
unhandledrejectionin browsers. - Mutation is opt-in: default to
toSorted/toReversed/withand spread;structuredCloneonly when depth is real. Runtime floors for all of these:modern.md. - Durations from
performance.now()(monotonic); wall-clock timestamps fromDate.now(). Never subtract twoDate.now()calls for benchmarks — NTP can step it backwards mid-measurement. - Sequential vs parallel is a decision you write down:
for...of+ await = sequential;Promise.all(arr.map(f))= parallel, fail-fast, and it does NOT cancel the losers. .lengthcounts UTF-16 code units, not characters:"😀".length === 2. Slice user-visible text withIntl.Segmenter, never by index.- Check the feature-floor table in
modern.mdbefore shipping ES2023+ APIs — onetoSortedcall breaks Node 18 at runtime, not at build time.
Numbers & Money
Number.MAX_SAFE_INTEGER= 2^53 − 1 = 9007199254740991 (16 digits). Integer ids with ≥16 digits can silently round inJSON.parse— transport ids as strings; use BigInt only for arithmetic (JSON.stringifyon BigInt throws).(1.005).toFixed(2) === "1.00"— binary representation, not a rounding bug you can fix locally. Compute in integer cents; display throughIntl.NumberFormat.- Pick the parser by intent:
Number("12px")→ NaN (whole-string strict),parseInt("12px")→ 12 (prefix).Number("")andNumber(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)).-0exists:Object.is(0, -0)is false,1 / -0 === -Infinity. It appears when rounding negatives toward zero and survives into keys and comparisons.
Dates & Time
- Months are 0-indexed:
new Date(2025, 0, 31)is Jan 31. - Parsing split: date-only ISO (
"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. - Month math rolls over:
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.- Store epoch ms (
Date.now()) plus IANA zone name; format only at the edge withIntl.DateTimeFormat.
Strings & Unicode
length,slice,charAtoperate on UTF-16 units:"👨👩👧".length === 8.[...str]yields code points; user-perceived characters needIntl.Segmenter. Index-based slicing can cut a surrogate pair → U+FFFD garbage.- Normalize before comparing user input: composed
"é"!=="e"+ combining accent even when rendered identically —str.normalize("NFC")both sides. - Human sorting:
(a, b) => a.localeCompare(b, undefined, {numeric: true})→["file2", "file10"], accents ordered correctly. Bare<compares code units ("Z" < "a"is true). /gand/yregexes are stateful:lastIndexpersists across calls, sore.test(s)twice on the same string can alternate true/false. Drop/gfor single tests or resetre.lastIndex = 0(depth:regex.md).
Objects, this & Closures
- Key order is spec'd: integer-like keys ascending FIRST, then strings in insertion order.
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. - User-controlled keys on plain objects are an injection surface:
obj[userKey]with"__proto__"pollutes the prototype. Use Map orObject.create(null)for user-keyed storage. Object.hasOwn(obj, k)overobj.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)detachesthis— wrap in an arrow.- Closure retention is per-scope in practice: one small long-lived closure keeps alive every object referenced by ANY sibling closure of that scope. Null out large locals before returning long-lived callbacks.
Timers & the Event Loop
- Microtasks (promise callbacks) drain completely before the next task: a self-scheduling microtask loop starves rendering and I/O forever; self-scheduling
setTimeoutyields between runs. - Timer clamps: nested
setTimeoutbeyond 5 levels → 4ms minimum; background tabs throttle to ≥1000ms (often far more). Clocks and animations must computeelapsed = Date.now() - starteach tick — accumulating+intervaldrifts. - Node: a
setTimeoutdelay above 2147483647 ms (~24.8 days, 32-bit signed) fires almost immediately. Long schedules: persist the target time and re-arm. - Node ordering:
process.nextTickqueue → promise microtasks → timers/macrotasks.setImmediatevssetTimeout(0): nondeterministic at top level,setImmediatealways first inside I/O callbacks. - Sync work over ~50ms is a long task (the input-jank threshold); a 60Hz frame budget is 16.7ms. Chunk batch work with an awaited yield (
await new Promise(r => setTimeout(r))) between slices.
Traps
| 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 |
Output Gates
Before emitting JS code or a review verdict, verify:
- Every promise created inside try is
return await-ed, notreturn-ed? - No API above the target floor (
modern.mdtable vsnode_floor/browser_floor)? - Money in integer minor units; no ≥16-digit id passing through
JSON.parseas a number? - Every thrown value an Error instance, wrapped with
causewhere context was added? - Each mutating call (
sort,reverse,splice,Object.assign) intentional, not incidental? - No plain object indexed by user-controlled keys?
- Concurrency written down: each await either deliberately sequential or batched with a bounded pool?
Where Experts Disagree
- Classes vs closures/factories. Classes when many instances share methods (prototype = one function object) or brand checks matter (
#x in obj); factories for one-off capability objects and simple DI. Field-initializer arrow functions are the worst of both (→modern.mdClasses). - Exceptions vs result values. Throw for the unexpected: broken invariants, failed I/O you cannot proceed without. Return values for expected outcomes: validation, not-found, cancellation. The test: if the immediate caller always try/catches, it wasn't exceptional — return it.
- Chained array methods vs loops. Chains are the readability default; a loop wins when profiling shows intermediate-array cost or the logic needs early exit beyond
some/every/find. Switching styles without a measurement is churn, not optimization.
Related Skills
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 behavior
Feedback
- If useful, star it: https://clawic.com/skills/javascript
- Latest version: https://clawic.com/skills/javascript
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/javascript.
Related skills
NodeJS
@ivangdavilaBuilds, debugs, and hardens Node.js servers, CLIs, and npm packages: async, modules, streams, memory, and process lifecycle. Use when writing or reviewing code that runs on Node, when a process hangs or refuses to exit, leaks memory, gets OOM-killed, pins one CPU core at 100%, or dies on an unhandled rejection; when the error reads EADDRINUSE, EMFILE, ECONNRESET, ERR_MODULE_NOT_FOUND, ERR_REQUIRE_ESM, or "__dirname is not defined"; when import and require interop breaks, streams buffer everything in RAM, a server returns intermittent 502s behind a load balancer, or SIGTERM drops in-flight requests; when npm install, lockfiles, peer dependencies, workspaces, native module builds, or publishing misbehave; when a suite passes locally and fails in CI; or when containerizing, profiling, and shutting down a service cleanly. Not for browser-only JavaScript, TypeScript type-system design, or the Bun and Deno runtimes.
TypeScript
@ivangdavilaWrites, reviews, and debugs type-safe TypeScript: type errors, narrowing, generics, tsconfig strictness, and declaration files. Use when a build hits type errors ("not assignable", "possibly undefined", "excessively deep", "cannot find module"), when eliminating any or unchecked as casts, designing generics or discriminated unions, writing .d.ts files or typing untyped npm packages, configuring tsconfig or module resolution, fixing ESM/CJS import crashes ("ERR_REQUIRE_ESM", "x is not a function"), validating API or JSON data at runtime, migrating JavaScript to TypeScript or upgrading the TypeScript version, or when tsc or the editor gets slow or runs out of memory.
Python
@ivangdavilaWrites, debugs, and reviews Python code — runtime traps, packaging, typing, async, tests, performance. Use when Python raises or misbehaves: ModuleNotFoundError, circular imports, AttributeError on None, UnboundLocalError, UnicodeDecodeError, mutable default arguments, `is` vs `==`, float rounding, naive vs aware datetimes, or a wrong answer with no exception; when pip, uv, poetry, venv, pyproject or a lockfile fight over dependencies, or a package installs but will not import; when threads, asyncio, multiprocessing or the GIL hang, deadlock, or leak memory; when pytest passes but should not, mocks patch the wrong module, or async tests never run; when mypy or pyright errors need clearing; when a script is slow, eats RAM, or gets OOM-killed; when subprocess calls hang or logs never appear; or when upgrading Python breaks the build. Not for library-specific problems — pandas, numpy, django, fastapi and flask have their own skills.
Rust
@ivangdavilaWrites, debugs, and optimizes Rust code, crates, and Cargo builds: ownership, lifetimes, traits, async, unsafe, and FFI. Use when the borrow checker rejects code, when rustc reports value moved, cannot borrow as mutable, does not live long enough, missing lifetime specifier, trait bound not satisfied, or not dyn compatible; when a future cannot be sent between threads, a Mutex guard crosses an await, a task blocks the async runtime, or select! loses data; when code deadlocks, panics on unwrap, or hits BorrowMutError; when cargo builds are slow, features unify unexpectedly, two versions of one crate collide, or the build fails only in CI; when writing unsafe, C FFI, proc macros, serde derives, no_std firmware, or wasm; when cross-compiling to musl or another target; when profiling, benchmarking, or shrinking a Rust binary. Not for C++ or language-agnostic concurrency theory.
SQL
@ivangdavilaWrites, reviews, and optimizes SQL queries; designs schemas, indexes, and constraints; plans migrations for any relational database. Use when a query is slow, EXPLAIN shows a sequential scan, or an index is ignored; when rows come back duplicated, missing, or with inflated totals after a JOIN; on deadlocks, lock timeouts, "too many connections", or transactions that never commit; when designing tables, keys, and column types, normalizing or denormalizing a model, or deciding between a JSON column and real columns; for ALTER TABLE on a live table, expand-migrate-contract rollouts, backups and restores, replication lag, connection pooling, partitioning, bulk CSV imports, and moving data between engines; for window functions, CTEs, keyset pagination, upserts, full-text search, multi-tenancy, row-level security, and timezone handling in MySQL, SQLite, MariaDB, or SQL Server. Not for PostgreSQL server internals such as vacuum tuning and work_mem sizing, and not for ORM schema modeling inside a framework.
MongoDB
@ivangdavilaDesigns MongoDB schemas, indexes, and aggregation pipelines, and debugs slow queries, connection errors, and replica set failures. Use when modeling documents, deciding embed vs reference, reading an explain plan, or fixing a COLLSCAN, and when a query times out, a pipeline aborts at the memory limit, a cursor dies mid-loop, the pool exhausts and server selection times out, writes fail with duplicate key or "not writable primary", a secondary lags, the oplog window closes, a shard key hotspots, or WiredTiger cache stalls the cluster. Covers mongosh and Compass, Atlas, Mongoose and driver connection strings, transactions and retry loops, change streams, time-series collections, Atlas Search and vector search, sharding, backups, restores, and upgrades. Not for SQL or relational modeling — normalization instincts actively mislead here.