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, 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 everyt...
Scanned 9/6/2026
Install to Claude Code
npx -y skills add clawic/skills --skill nodejs --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Nodejs?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/clawic-nodejs)More formats (shields.io, HTML) on the badges page.
---
name: NodeJS
slug: nodejs
version: 1.0.4
description: '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, 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.'
homepage: https://clawic.com/skills/nodejs
changelog: 'Full coverage pass: deeper guides, situation-named files, and per-user configuration'
metadata:
clawdbot:
emoji: 💚
requires:
bins:
- node
os:
- linux
- darwin
- win32
displayName: NodeJS
configPaths:
- ~/Clawic/data/nodejs/
- ~/nodejs/
- ~/clawic/nodejs/
openclaw:
requires:
config:
- ~/Clawic/data/nodejs/
- ~/nodejs/
- ~/clawic/nodejs/
---
User 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/`, and say in one line that you moved it and from where.
## 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
1. **Budget sync work at ~10 ms per slice.** Max throughput per process ≈ 1000 ms ÷ block_ms: a 50 ms sync `JSON.parse` in 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 with `setImmediate` (→ `async.md`).
2. **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.
3. **"Async" fs, dns.lookup, crypto, and zlib share 4 libuv threads** (default `UV_THREADPOOL_SIZE`; max 1024). The 5th concurrent `fs.promises.readFile` queues 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.
4. **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.
5. **`keepAliveTimeout` must 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 s` and `headersTimeout = keepAliveTimeout + 1 s` (ALB 60 s → 65 s / 66 s; nginx upstream `keepalive_timeout 75s` → 80 s / 81 s).
6. **Exact versions for apps, ranges for libraries; `npm ci` in anything automated.** `npm install` rewrites the lockfile whenever package.json allows it — in CI that means testing an install nobody reviewed (→ `packages.md`).
7. **One process per container.** Let the orchestrator scale replicas; `cluster` only on bare metal or VMs you own entirely — two schedulers fighting over the same cores obscures both.
8. **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 with `FATAL ERROR: ... heap out of memory` means 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): `keepAliveTimeout` 5 s, `headersTimeout` 60 s, `requestTimeout` 300 s, `server.timeout` 0 (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 under `requestTimeout`.
- Shutdown backstop = orchestrator grace window − 5 s (Kubernetes `terminationGracePeriodSeconds` defaults 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 `*Sync` call, unbounded `JSON.parse`, or whole-array loop reachable from a request handler? (rule 1)
- Every `Promise.all` bounded by something other than user input? (rule 4)
- Every stream wired through `pipeline()` or carrying its own `error` handler — an unhandled stream `error` is a process crash?
- Every outbound call carrying a timeout or AbortSignal, and every server timeout ordered per the ladder?
- All `process.env` reads 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 --run` vs package-manager scripts), linter/formatter stack, profiler of choice — affects the shape of every command example
- **Conventions** — extensions (`.mjs`/`.cjs` vs a `type` field), error-class taxonomy, script names, folder layout, log field names
- **Platform** — OS (Windows path and `spawn` caveats), 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-scripts` on 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 (`instanceof` fails across copies, module state duplicates); ESM-only is viable once consumers are ESM or run node >=22.12, where `require()` of a sync ESM graph works.
- **`node:test` vs Jest vs Vitest.** `node:test` for 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 `tsc` build stays correct for published libraries that must ship `.d.ts` and 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, regex
- `typescript` — type-system design, tsconfig strictness, declaration files
- `docker` — containerizing the service, image layers, resource limits
- `api` — 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](https://clawic.com), the verified skill library. Get this skill: https://clawic.com/skills/nodejs.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!