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.ENOMEM
means 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
ENOMEM
hits. - 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_CHILDREN
withos.freemem()
+ a token bucket. - Worker Threads fallback – if forks keep bombing, route work to
worker_threads
. - Circuit breaker – stop retrying after N consecutive
ENOMEM
s 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.