Hi, I’m the developer behind this client project, and today I’m walking you through a painful crash using node.js I met head on and the little utility I wrote so you (and future‑me) don’t have to lose another afternoon to it.
Error Code
I was piping thousands of photos through zbarimg to extract QR codes when the whole script keeled over with Node.js:
child_process.js:935
throw errnoException(process._errno, 'spawn');
^
Error: spawn ENOMEM
No 'error' or 'close' events, just an uncaught exception and a dead process. After a bit of digging I realised:
spawn()throws synchronously when the OS refuses to fork.ENOMEMmeans the kernel can’t (or won’t) allocate memory for the child.
So the fix wasn’t in my event listeners it was in how I called spawn().
What Triggers ENOMEM?
| Trigger | What’s Really Happening |
|---|---|
| System memory pressure | The kernel can’t over‑commit more RAM. |
| Per‑process limits | ulimit -v, ulimit -m (Linux) or Job‑Objects (Windows) block the fork. |
| FD exhaustion (rare) | Some platforms reuse ENOMEM when no FDs are left. |
A Minimal Reproducer
const { spawn } = require('child_process');
try {
// ENOMEM is thrown *here* if the fork fails
const zbarimg = spawn('zbarimg', [photo, '-q']);
// These will never fire if the throw already happened
zbarimg.on('error', err => console.error('[child] error:', err));
zbarimg.on('close', code => console.log('[child] close', code));
} catch (err) {
if (err.code === 'ENOMEM') {
console.error('[parent] Not enough memory to spawn zbarimg');
// Optional: back‑off, alert, or queue the job
} else {
throw err; // keep other errors noisy
}
}
Key takeaway: always wrap spawn() in node.js a try…catch if the child is mission‑critical.
Level‑Up: spawnSafe() in 40 Lines
My QR‑processing pipeline can launch dozens of workers, so I needed more than a single try…catch. I wanted:
- A cap on concurrent children (so I don’t melt the box).
- Automatic retries with exponential back‑off when
ENOMEMhits. - Regular errors to behave normally.
Here’s the helper I dropped into the project:
/**
* spawnSafe(cmd, args?, opts?) → Promise<ChildProcess>
*
* • Limits concurrent forks (default = CPU cores - 1)
* • Retries ENOMEM up to `maxRetries` with back‑off
* • Rejects on any other sync or runtime error
*/
const { spawn } = require('child_process');
const os = require('os');
const MAX_CHILDREN = Math.max(1, os.cpus().length - 1);
class ForkLimiter {
constructor(limit = MAX_CHILDREN) {
this.limit = limit;
this.active = 0;
this.waiting = [];
}
async exec(fn) {
if (this.active >= this.limit) {
await new Promise(r => this.waiting.push(r));
}
this.active++;
try { return await fn(); }
finally {
this.active--;
if (this.waiting.length) this.waiting.shift()();
}
}
}
const limiter = new ForkLimiter();
function spawnSafe(cmd, args = [], opts = {}) {
const { maxRetries = 3, baseDelay = 200 } = opts;
return limiter.exec(() => new Promise((resolve, reject) => {
let attempt = 0;
const fork = () => {
try {
const child = spawn(cmd, args, opts);
child.once('error', reject); // runtime errors
child.once('exit', (code, sig) =>
code === 0
? resolve(child)
: reject(new Error(`${cmd} exited ${code} (sig ${sig})`))
);
} catch (err) { // sync errors
if (err.code === 'ENOMEM' && attempt < maxRetries) {
setTimeout(fork, baseDelay * 2 ** attempt++);
} else {
reject(err);
}
}
};
fork();
}));
}
module.exports = spawnSafe;
Real World Use
(async () => {
try {
const photo = '/tmp/qr.jpg';
const proc = await spawnSafe('zbarimg', [photo, '-q']);
let data = '';
proc.stdout.on('data', chunk => data += chunk);
proc.stderr.pipe(process.stderr);
proc.on('close', () => console.log('QR:', data.trim()));
} catch (e) {
console.error('Failed to decode:', e);
}
})();
In production the helper shaved my crash rate to zero and kept CPU usage sane under load.
Practice Ideas
Want to stretch this further? Here’s what I’m tinkering with next:
- Dynamic concurrency – scale
MAX_CHILDRENwithos.freemem()+ a token bucket. - Worker Threads fallback – if forks keep bombing, route work to
worker_threads. - Circuit breaker – stop retrying after N consecutive
ENOMEMs for T seconds. - Durable queue – stash pending jobs in Redis so a deploy doesn’t nuke the backlog.
- Prom‑metrics –
spawn_attempt_total,spawn_enomem_total,spawn_retry_total.
Each bullet flexes a different production grade muscle resource governance, resilience, observability.
Final Thought
I learned the hard way that spawn() node.js can crash your whole app before any events fire. Wrapping it in try…catch is table‑stakes; adding a thin safety layer like spawnSafe() turns it into a well‑behaved citizen under memory pressure.

