Common Node.js bugs and gotchas — a field reference

Every bug in this lesson has already been explained mechanically somewhere earlier in this domain — this is the field-reference version, the shape each one actually takes in real code, so it's recognizable on sight instead of requiring the mechanism to be re-derived from scratch every time.

Advanced

4 min read

Bug 1: a synchronous call blocking every other request

app.get("/report", (req, res) => {
  const data = fs.readFileSync("./huge-report.csv"); // blocks EVERY other request on the server
  res.send(data);
});

Covered mechanically in the fs-module lesson: readFileSync blocks the single thread for its entire duration — no other request, no matter how simple, can even begin being processed until it finishes. The fix: the async fs.readFile or fs/promises form, always, for anything running inside a server's request path.

Bug 2: a forgotten next() hanging a request forever

app.use((req, res, next) => {
  console.log("logging the request");
  // missing next() — the request just hangs, no timeout, no error
});

Covered mechanically in the middleware-chains lesson: nothing advances the chain automatically — a middleware that does its work but forgets next() (and doesn't send a response itself) leaves the request stuck indefinitely. The fix: audit every middleware's code paths, especially early-return branches inside conditionals, for a missing next() call.

Bug 3: assuming in-memory state is shared across cluster workers

const cache = new Map(); // in a CLUSTERED app, EVERY worker has its OWN separate Map
app.get("/data", (req, res) => {
  if (cache.has(req.query.key)) return res.json(cache.get(req.query.key)); // only hits if THIS worker cached it
});

Covered mechanically in the clustering lesson: each cluster worker is a genuinely separate OS process, sharing no memory at all — a value cached by worker 2 simply doesn't exist in worker 1's memory. The fix: shared, cross-worker state (sessions, caches) needs to live somewhere genuinely external — Redis, a real database — not in any individual worker's own process memory.

Bug 4: a request cut off mid-response during a routine deploy

// No SIGTERM handler at all — the default behavior on receiving SIGTERM
// is to exit close to immediately, dropping whatever's in flight

Covered mechanically in the graceful-shutdown lesson: without an explicit SIGTERM handler, a deploy or scale-down kills the process mid-request by default, a real, user-visible failure during what should be routine. The fix: server.close() to stop accepting new connections while letting in-flight ones finish, with a bounded force-exit timeout as a safety net against a hang.

Bug 5: exec's output buffer silently truncating (or erroring on) large output

exec("find / -type f", (err, stdout) => { /* output can EXCEED exec's default buffer limit */ });

Covered mechanically in the child-processes lesson: exec buffers the entire output in memory before calling back, and has a real, default maximum buffer size — a command producing more output than that limit causes an error. The fix: spawn, which streams output chunk by chunk with no such buffer ceiling, for any command with genuinely large or unbounded output.

Bug 6: a CPU-heavy computation freezing the entire server

app.get("/analyze", (req, res) => {
  const result = runExpensiveAnalysis(req.body); // BLOCKS the event loop — every other request waits
  res.json(result);
});

Covered mechanically in the worker-threads lesson: non-blocking I/O never helps genuinely CPU-bound work, since there's no idle waiting period to reclaim — the single thread stays busy the entire computation. The fix: offload the heavy computation to a worker_threads worker, freeing the main thread to keep handling other (I/O-bound) requests concurrently.

The actual throughline across all six

Every one of these traces back to the same handful of mechanisms this domain already covered in depth: Node's single-threaded event loop being genuinely blockable by synchronous or CPU-heavy work, the middleware chain's explicit next()-driven execution, and cluster workers' complete lack of shared memory. Recognizing a bug's shape on sight — "this smells like a blocked event loop," "this smells like cross-worker state that doesn't actually exist" — is what separates fixing a Node.js production issue quickly from re-deriving these mechanisms from first principles every single time one shows up.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does calling `fs.readFileSync()` inside a request handler block every OTHER incoming request, not just the current one?

2. Why does an in-memory cache work fine in a single-process app but fail confusingly once the app is clustered?

3. What's the actual throughline connecting all six bugs in this field-reference lesson?