Event Loop
fs.readFileSyncblocks entire server — usefs.promises.readFileor callback version- CPU-intensive code blocks — offload to worker threads or child process
setImmediatevsprocess.nextTick— nextTick runs before I/O, setImmediate after- Long-running loops starve I/O — break into chunks with setImmediate
Async Error Handling
- Unhandled promise rejection crashes in Node 15+ — always
.catch()or try/catch with await process.on('unhandledRejection')for global handler — log and exit gracefully- Errors in callbacks need explicit handling — won't propagate to outer try/catch
Promise.allfails fast — one rejection rejects all, usePromise.allSettledif need all results
CommonJS vs ESM
"type": "module"in package.json for ESM — otherwise.mjsextension- ESM:
import x from 'y'— CommonJS:const x = require('y') - No
__dirnamein ESM — useimport.meta.urlwithfileURLToPath - Can't
require()ESM modules — use dynamicimport()which returns Promise exportsis reference tomodule.exports— reassigningexports = xbreaks it
Environment Variables
process.envvalues are always strings —PORT=3000is"3000"not3000- Missing env var is
undefined, not error — check explicitly or use defaults .envfiles needdotenv— not built-in, calldotenv.config()early- Don't commit
.env— use.env.examplewith dummy values
Memory Leaks
- Event listeners accumulate —
removeListenerwhen done, or useonce - Closures capturing large objects — nullify references when done
- Global caches grow unbounded — use LRU cache with size limit
--max-old-space-sizeto increase heap — but fix leaks first
Streams
- Backpressure:
write()returns false when buffer full — wait fordrainevent .pipe()handles backpressure automatically — prefer over manual read/write- Error handling on all streams —
stream.on('error', handler)or pipeline errors silently pipeline()over.pipe()— handles errors and cleanup properly
Common Mistakes
JSON.parsethrows on invalid JSON — wrap in try/catchrequire()is cached — same object returned on repeated calls- Circular dependencies partially work — but export may be incomplete at require time
asyncfunction always returns Promise — even if you return plain valueBuffer.from(string)encoding matters — default is UTF-8, specify if different