NodeJS
Builds, 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, w…
Iván
@ivangdavila
What This Skill Does
Diagnoses and fixes Node.js runtime issues including async hangs, memory leaks, stream backpressure, module resolution errors, and process lifecycle problems. Covers debugging live processes, optimizing server performance, resolving npm package conflicts, and ensuring clean shutdowns.
Replaces trial-and-error debugging of Node.js production incidents by providing targeted playbooks for common failure modes like event-loop stalls, OOM kills, EMFILE errors, and module-format mismatches.
When to Use It
- Debug a Node process that hangs or refuses to exit after completing its work
- Investigate a server returning intermittent 502 errors behind a load balancer
- Fix an ERR_REQUIRE_ESM or __dirname is not defined error in a mixed ESM/CJS project
- Resolve npm ci failures caused by a lockfile out of sync with package.json
- Profile a Node service that pins one CPU core at 100% and slows all requests
- Diagnose a container that gets OOMKilled overnight despite stable heap usage
Install
$ openclaw skills install @ivangdavila/nodejsUser preferences and memory live in ~/Clawic/data/nodejs/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/nodejs/ or ~/clawic/nodejs/), move it to ~/Clawic/data/nodejs/.
When To Use
- Writing or reviewing Node services, CLIs, libraries, workers, or release scripts
- Debugging a live process: hangs, crash loops, growing RSS, slow endpoints, import failures, unclean shutdown
- Deciding the runtime shape of a package: ESM vs CJS, exports map, lockfile policy, native dependencies, publish surface
- Production incidents: intermittent 502s behind a proxy, EMFILE, event loop stalls, containers OOMKilled overnight
- Running TypeScript, tests, or child processes on Node — the runtime side, not the type system
- Not for browser-only JavaScript, TypeScript type design, or the Bun/Deno runtimes (each has its own skill)
Quick Reference
| Situation | Play |
|---|---|
| One core pinned, every request slows at once | Event loop blocked. Record node --cpu-prof, read widest self-time frames, not deepest stacks → performance.md |
| Process finishes its work but never exits | An open handle: server, socket, setInterval, worker, or pool. process.getActiveResourcesInfo() names it → debug.md |
RSS climbs while heapUsed stays flat | Buffers/native memory, not a JS leak — heap snapshots will show nothing → performance.md |
| Intermittent 502/504 behind ALB or nginx | keepAliveTimeout below the proxy's idle timeout (rule 5) → http.md |
ERR_REQUIRE_ESM, ERR_MODULE_NOT_FOUND, __dirname is not defined | Module-format mismatch or a missing file extension → modules.md |
EADDRINUSE on restart | Old process still holding the port, or two workers binding it; in tests, bind port 0 → debug.md |
EMFILE / ENFILE | Unbounded concurrency or leaked descriptors (rule 4) → filesystem.md |
Exit 134 with FATAL ERROR: ... heap out of memory | V8's heap ceiling, not the container's limit (rule 8) → performance.md |
| Memory grows during a copy, upload, or export | Backpressure ignored: write() returned false and nobody waited → streams.md |
npm install works, npm ci fails | Lockfile out of sync with package.json (rule 6) → packages.md |
| Native module fails to build or load after an upgrade | ABI mismatch, missing toolchain, or musl vs glibc → runtime.md |
| Child process hangs, or its output is truncated | maxBuffer exceeded, or nobody drains the stdio pipe → concurrency.md |
A .ts file won't run, or imports resolve at build but not at runtime | Type-stripping limits or moduleResolution mismatch → typescript.md |
CLI crashes with EPIPE when piped into head | stdout closed early; handle the error instead of letting it throw → cli.md |
| Suite green locally, red in CI | Port collisions, fake timers, shared module state, file ordering → testing.md |
Untrusted input reaching exec, a path join, or a regex | Injection, traversal, ReDoS — the exact checks → security.md |
| SIGTERM drops in-flight requests on deploy | No drain sequence, or node is PID 1 with no signal handler → production.md |
| Anything else | Reproduce with the smallest script that still fails, then re-add flags, env vars, and imports one at a time |
Depth on demand: debug.md symptom→cause chains · commands.md diagnostic toolkit · async.md event loop and promises · modules.md ESM/CJS · errors.md failure handling and shutdown · streams.md backpressure · http.md servers, clients, timeouts · performance.md profiling and leaks · concurrency.md workers, cluster, child processes · filesystem.md files, paths, descriptors · security.md hardening · testing.md suites and mocks · packages.md npm, lockfiles, publishing · typescript.md running TS on Node · runtime.md versions, flags, native modules · production.md deploy, config, logging, observability · cli.md command-line tools.
Core Rules
- Budget sync work at ~10 ms per slice. Max throughput per process ≈ 1000 ms ÷ block_ms: a 50 ms sync
JSON.parsein a request handler caps that process near 20 req/s, and every concurrent request inherits the stall. Repeatedly over budget →worker_threads; one-off → partition withsetImmediate(→async.md). - Handle operational errors, crash on programmer errors (Joyent doctrine). ECONNRESET, ENOENT, bad user input → handle at the call site. TypeError, undefined property → let it crash; the supervisor restarts a clean process. Catch-all recovery keeps corrupted state serving traffic.
- "Async" fs, dns.lookup, crypto, and zlib share 4 libuv threads (default
UV_THREADPOOL_SIZE; max 1024). The 5th concurrentfs.promises.readFilequeues behind the first 4. Slow "async" crypto/fs under load → raise the pool to roughly the number of concurrent slow calls, or move the work off the process. - Cap concurrency explicitly.
Promise.all(items.map(fetch))over 10k items opens 10k sockets — EMFILE at the common 1024 fd soft limit. Batch or use a limiter; 8-32 concurrent per upstream host is a sane starting cap, raised on evidence: throughput climbing with flat latency = raise it; latency climbing = you found the ceiling. keepAliveTimeoutmust exceed the proxy's idle timeout. Node's default is 5 s; ALB's default idle is 60 s, so Node closes sockets the balancer just reused → intermittent 502s. Formula:keepAliveTimeout = proxy_idle + 5 sandheadersTimeout = keepAliveTimeout + 1 s(ALB 60 s → 65 s / 66 s; nginx upstreamkeepalive_timeout 75s→ 80 s / 81 s).- Exact versions for apps, ranges for libraries;
npm ciin anything automated.npm installrewrites the lockfile whenever package.json allows it — in CI that means testing an install nobody reviewed (→packages.md). - One process per container. Let the orchestrator scale replicas;
clusteronly on bare metal or VMs you own entirely — two schedulers fighting over the same cores obscures both. - Set the heap ceiling below the memory limit. RSS = heap + buffers + stacks + native, so a default-sized heap under a container limit invites an OOMKill instead of a catchable heap error. Formula:
--max-old-space-size = floor(0.75 × container_limit_MB)— 512 MB limit → 384; 2 GB → 1536. Exit 134 withFATAL ERROR: ... heap out of memorymeans you hit V8's ceiling; exit 137 means the kernel hit yours first.
Exit and Error Codes
Exit codes above 128 mean killed by signal code − 128; Node's own fatal codes sit below 10.
| Code | Meaning | First move |
|---|---|---|
| 1 | Uncaught fatal exception | Read the stack; an empty one means a non-Error was thrown (→ Traps) |
| 3 / 5 | Internal JS parse failure / fatal V8 error | Almost always a bad --flag or a corrupt install, not your code |
| 7 | Exception inside an exception handler | Your uncaughtException hook itself threw |
| 9 | Invalid argument to the node binary | Flag typo, or a flag this major doesn't support (→ runtime.md) |
| 130 | SIGINT (128+2) | Ctrl-C; clean unless a handler swallowed it |
| 134 | SIGABRT (128+6) | V8 fatal — usually heap OOM (rule 8) or a failed native assertion |
| 137 | SIGKILL (128+9) | Kernel or orchestrator OOM kill, or a stop timeout expiring (rule 8) |
| 143 | SIGTERM (128+15) | Normal stop; if requests were dropped, the drain sequence is missing (→ production.md) |
Error codes worth recognizing on sight: EADDRINUSE (port held), EMFILE/ENFILE (descriptor limit, rule 4), ECONNRESET (peer closed mid-flight — operational, rule 2), EPIPE (downstream closed a pipe), ERR_REQUIRE_ESM and ERR_MODULE_NOT_FOUND (format or extension), ERR_UNHANDLED_REJECTION (rule 2), ERR_STRING_TOO_LONG (V8 caps string length in the hundreds of MB — stream instead of JSON.stringify), ERR_CHILD_PROCESS_STDIO_MAXBUFFER (child output over the buffer cap).
Timeout Ladder
Every timeout in the path must be ordered, or one layer kills a connection another layer still believes in. Each line reads strictly larger left to right:
client abort < upstream call timeout < inbound requestTimeout
proxy idle < keepAliveTimeout < headersTimeout
force-exit backstop < orchestrator kill window
- Node HTTP server defaults (node >=18):
keepAliveTimeout5 s,headersTimeout60 s,requestTimeout300 s,server.timeout0 (disabled). Only the first is short enough to bite by accident (rule 5). - Outbound calls need their own deadline — an upstream that never answers holds a socket, a descriptor, and a request slot.
AbortSignal.timeout(ms), with retries × per-try timeout still underrequestTimeout. - Shutdown backstop = orchestrator grace window − 5 s (Kubernetes
terminationGracePeriodSecondsdefaults to 30 s → force-exit at 25 s), so you exit on your own terms with logs flushed instead of by SIGKILL.
Version Gates
The canonical minimums this skill's guidance assumes. Check node -v before recommending anything above the user's line, and name the fallback when the target is older.
| Feature | Minimum |
|---|---|
| Unhandled rejection terminates the process (default) | 15 |
new Error(msg, { cause }) | 16.9 |
AbortSignal.timeout(), process.getActiveResourcesInfo() | 17.3 |
Global fetch/undici (stable from 21) | 18 |
node:test runner stable | 20 |
--env-file=.env built in | 20.6 |
import.meta.dirname / import.meta.filename | 20.11 |
node --run <script>, --watch stable | 22 |
require() of a synchronous ESM graph | 22.12 |
Running .ts by type stripping, unflagged | 24 (backported to 22.18) |
Release cadence, so "supported" stays checkable instead of remembered: one new major every April and October; even majors enter LTS that October and get ~3 years total (6 months Current, 12 Active LTS, 18 Maintenance); odd majors die after ~6 months and never belong in production. Which majors are live today: runtime.md (dated).
Output Gates
Before shipping Node code, verify:
- Any
*Synccall, unboundedJSON.parse, or whole-array loop reachable from a request handler? (rule 1) - Every
Promise.allbounded by something other than user input? (rule 4) - Every stream wired through
pipeline()or carrying its ownerrorhandler — an unhandled streamerroris a process crash? - Every outbound call carrying a timeout or AbortSignal, and every server timeout ordered per the ladder?
- All
process.envreads parsed and validated once at startup — they are strings or undefined, never numbers or booleans? - Shutdown path present: SIGTERM → fail readiness → stop accepting → drain → close pools → exit, in that order (→
errors.md)? - Heap ceiling set against the container limit (rule 8), and one process per container (rule 7)?
Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/nodejs/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| package_manager | npm | pnpm | yarn | bun | npm | Every install, lockfile, workspace, and CI command; which npm ci equivalent rule 6 names |
| module_system | esm | cjs | esm | Import syntax and extensions in all examples, type field guidance, which half of modules.md leads |
| node_target | number (major, >=20) | 24 | Which APIs the guidance may use without a fallback — read against the Version Gates table |
| test_runner | node | jest | vitest | node | Test, mock, and fake-timer idioms; the coverage and watch flags quoted in testing.md |
| deploy_target | container | vm | serverless | paas | container | Signal and PID-1 handling, cluster vs replicas (rule 7), heap sizing (rule 8), shutdown window |
| ts_runner | none | strip | tsx | tsc | none | Whether .ts guidance assumes type stripping, a loader, or a build step (typescript.md) |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:
- Tooling — script runner (
node --runvs package-manager scripts), linter/formatter stack, profiler of choice — affects the shape of every command example - Conventions — extensions (
.mjs/.cjsvs atypefield), error-class taxonomy, script names, folder layout, log field names - Platform — OS (Windows path and
spawncaveats), architecture (arm64 prebuilds), base image (musl vs glibc), registry mirror - Thresholds — concurrency cap per upstream (rule 4), heap fraction (rule 8), timeout values, stream
highWaterMark - Risk posture — crash-vs-catch aggressiveness (rule 2),
--ignore-scriptson install, automatic major bumps, permission model - Output format — log shape (JSON vs pretty), verbosity, how much error detail reaches clients
- Work order — test-first vs code-first, profile-before-optimize, review gates before publish
- Integrations — registry, observability vendor, CI provider, process supervisor
- Constraints — banned dependencies, license policy, no-native-modules, offline or air-gapped installs
- Cadence — dependency update rhythm, Node upgrade rhythm against the April/October cadence
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
| Unhandled promise rejection | Terminates the process by default (node >=15) | .catch() or try/catch on every await chain |
.pipe() without error wiring | Errors don't cross pipes; the source leaks, then crashes | stream.pipeline() |
Forgotten await inside try/catch | The rejection skips the catch and surfaces later, or never | Lint with no-floating-promises |
exports = x in CJS | Rebinds a local alias; module.exports is unchanged | module.exports = x |
__dirname in ESM | Doesn't exist there | import.meta.dirname, else fileURLToPath(import.meta.url) |
Mutating a require()d object | Module cache: every consumer sees the mutation | Export factories, or freeze exports |
process.env.PORT used as a number | Env values are strings ("3000") or undefined | Parse and validate the whole env once at startup |
| Listeners added per request | Accumulate to MaxListenersExceededWarning — a leak signal, not a limit | once(), or remove on completion |
| Raising the max-listeners limit to silence that warning | Hides the leak that produced it; memory keeps climbing | Find who adds a listener per event and remove it |
| Throwing non-Error values | No stack; instanceof Error checks fail; exit 1 with an empty trace | throw new Error(msg, { cause }) |
node:vm as a sandbox | Not a security boundary — its own docs say so | Isolate untrusted code at process or container level |
npm audit fix --force on a red report | Applies major bumps that break the app to silence findings in code that may never run | Triage by reachability, pin with overrides (→ packages.md) |
node index.js as PID 1 in a container | PID 1 gets no default signal handlers: SIGTERM is ignored and every stop burns the full grace window | Register a SIGTERM handler, or run an init (→ production.md) |
Where Experts Disagree
- Crash-and-restart vs catch-and-continue. Crash when a supervisor restarts you and in-process state may be corrupt; continue only for operational errors scoped to a single request. The boundary question: can you name exactly what stayed consistent?
- Dual CJS+ESM packages vs ESM-only. Dual carries the two-instance hazard (
instanceoffails across copies, module state duplicates); ESM-only is viable once consumers are ESM or run node >=22.12, whererequire()of a sync ESM graph works. node:testvs Jest vs Vitest.node:testfor new libraries — no transform layer, native ESM, no hoisting magic; Jest for existing suites with heavy matcher and snapshot use; Vitest where the project already runs a Vite pipeline. Migrating a green suite for aesthetics is a net loss.- TypeScript at runtime vs a build step. Type stripping deletes the toolchain for apps deployed from source; a
tscbuild stays correct for published libraries that must ship.d.tsand support older consumers.
Related Skills
More Clawic skills, get them at https://clawic.com/skills/nodejs (install if the user confirms):
javascript— language-level semantics: coercion, closures, dates, regextypescript— type-system design, tsconfig strictness, declaration filesdocker— containerizing the service, image layers, resource limitsapi— HTTP API design, versioning, and contracts above the runtime
Feedback
- If useful, star it: https://clawic.com/skills/nodejs
- Latest version: https://clawic.com/skills/nodejs
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/nodejs.
Top skills in this category
Skill Vetter
@spclaudehomeSecurity-first skill vetting for AI agents. Use before installing any skill from ClawdHub, GitHub, or other sources. Checks for red flags, permission scope, and suspicious patterns.
Github
@steipeteInteract with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Humanizer
@biostartechnologyRemove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's comprehensive "Signs of AI writing" guide. Detects and fixes patterns including: inflated symbolism, promotional language, superficial -ing analyses, vague attributions, em dash overuse, rule of three, AI vocabulary words, negative parallelisms, and excessive conjunctive phrases.
Free Ride - Unlimited free AI
@shaivpidadiManages free AI models from OpenRouter for OpenClaw. Automatically ranks models by quality, configures fallbacks for rate-limit handling, and updates opencla...
Elite Longterm Memory
@nextfrontierbuildsUltimate AI agent memory system for Cursor, Claude, ChatGPT & Copilot. WAL protocol + vector search + git-notes + cloud backup. Never lose context again. Vibe-coding ready.