node-async
Skill1 starsChanged 27 days agoMITView on GitHub
Node.js async patterns advisor. Always use this skill when working with async/await, Promises, the event loop, streams, backpressure, callbacks, or concurrent operations in Node.js. Use when the user asks about unhandled promise rejections, blocking the event loop, Promise.all vs Promise.allSettled, slow Node.js under load, stream backpressure, or says "my Node app lags under load", "event loop is blocked", "how do I implement backpressure", "unhandled promise rejection", or "concurrent requests". Read this before debugging any Node.js async or concurrency issue.
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 74
Run independent async operations concurrently: don't await them sequentially when order doesn't matter.
- Distinctive: line 130
Prefer async/await. It's easier to read, easier to debug (stack traces are meaningful), and error handling is straightforward with …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Node.js Async Patterns
Node.js runs on a single thread. Everything async works because the event loop defers I/O. But that model breaks the moment you block the thread or mismanage promises. Most Node.js production issues trace back to one of a handful of async mistakes.
Never block the event loop
The event loop handles every request. If you block it (with a CPU-intensive loop, a synchronous file read, or heavy JSON parsing), every in-flight request stalls until you're done.
// Bad: blocks the event loop for every caller
app.get("/report", (req, res) => {
const result = fs.readFileSync("large-file.csv"); // synchronous — blocks
res.send(process(result));
});
// Good: yields to the event loop
app.get("/report", async (req, res) => {
const result = await fs.promises.readFile("large-file.csv");
res.send(process(result));
});CPU-bound work (parsing, hashing, compression) belongs in a Worker Thread, not the event loop. Offload it:
import { Worker } from "worker_threads";
function runInWorker(data: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const worker = new Worker("./worker.js", { workerData: data });
worker.on("message", resolve);
worker.on("error", reject);
});
}Handle every rejected promise
In modern Node.js (v15+), an unhandled promise rejection crashes the process. In older versions it silently swallows the error, often worse.
// Bad: rejection is swallowed if sendEmail rejects
async function notify(user: User) {
sendEmail(user.email, "Welcome!"); // fire and forget — dangerous
}
// Good: either await it or explicitly handle the rejection
async function notify(user: User) {
await sendEmail(user.email, "Welcome!");
}
// Or if you genuinely mean fire-and-forget, handle the rejection explicitly
sendEmail(user.email, "Welcome!").catch((err) =>
logger.error("email failed", err),
);Listen for the process-level safety net, but don't rely on it as a substitute for proper handling:
process.on("unhandledRejection", (reason) => {
logger.error("Unhandled rejection", reason);
process.exit(1); // exit rather than continue in unknown state
});Concurrent operations: Promise.all and Promise.allSettled
Run independent async operations concurrently: don't await them sequentially when order doesn't matter.
// Bad: sequential — total time = sum of all durations
const user = await fetchUser(id);
const orders = await fetchOrders(id);
const prefs = await fetchPrefs(id);
// Good: concurrent — total time = slowest of the three
const [user, orders, prefs] = await Promise.all([
fetchUser(id),
fetchOrders(id),
fetchPrefs(id),
]);Promise.all vs Promise.allSettled:
Promise.all: rejects immediately if any promise rejects. Use when all results are required.Promise.allSettled: waits for all, gives you success/failure for each. Use when partial results are acceptable.
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
for (const result of results) {
if (result.status === "fulfilled") use(result.value);
else logger.warn("fetch failed", result.reason);
}Async iteration and streams
For large data, streams beat loading everything into memory. Use async iteration (Node 12+) to consume them cleanly:
import { createReadStream } from "fs";
import { createInterface } from "readline";
async function processLines(path: string) {
const rl = createInterface({ input: createReadStream(path) });
for await (const line of rl) {
process(line); // one line at a time — constant memory
}
}Watch for back-pressure: if you write to a writable stream faster than it drains, you'll buffer unbounded data in memory. Respect the return value of write() and wait for the drain event, or use pipeline():
import { pipeline } from "stream/promises";
await pipeline(readableSource, transformStream, writableDestination);pipeline() handles back-pressure and cleanup automatically. Prefer it over manually piping.
async/await over raw Promises
Prefer async/await. It's easier to read, easier to debug (stack traces are meaningful), and error handling is straightforward with try/catch.
// Harder to follow; error handling scattered across .catch chains
fetchUser(id)
.then((user) => fetchOrders(user.id))
.then((orders) => process(orders))
.catch((err) => handle(err));
// Easier: linear flow, single error handler
try {
const user = await fetchUser(id);
const orders = await fetchOrders(user.id);
process(orders);
} catch (err) {
handle(err);
}One exception: when composing promises at a higher level (e.g., Promise.all), raw promise APIs are still natural.
Avoid async in constructors
Constructors can't be async. If initialization requires async work, use a static factory method:
// Bad: constructor silently ignores the async work
class Database {
constructor() {
this.connect(); // fire-and-forget — db may not be ready when used
}
}
// Good: factory that returns a fully-initialized instance
class Database {
private constructor(private conn: Connection) {}
static async create(): Promise<Database> {
const conn = await openConnection();
return new Database(conn);
}
}
const db = await Database.create();What to watch for in code review
awaitinside a loop wherePromise.allwould work (sequential when concurrent is safe)- Promises that aren't awaited and don't have
.catch()handlers - Synchronous fs/crypto/zlib calls inside request handlers
- Heavy CPU work (parsing, hashing large data) on the event loop thread
- Streams being consumed via
.on('data')without back-pressure handling
In this repository25 other files it gives its agents.
- api-designSkill
- arch-reviewSkill
- db-reviewSkill
- debugSkill
- dotnet-asyncSkill
- dotnet-opsSkill
- dotnet-testingSkill
- dotnet-typesSkill
- go-concurrencySkill
- go-opsSkill
- go-testingSkill
- go-typesSkill
- incidentSkill
- node-opsSkill
- perfSkill
- pr-prepSkill
- py-asyncSkill
- py-opsSkill
- py-testingSkill
- py-typesSkill
- refactorSkill
- securitySkill
- test-genSkill
- ts-testingSkill
- ts-typesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

