
Mastering Error Handling in Node.js. UnhandledRejection, uncaughtException, and Beyond
In Node.js, errors don’t just crash your app - they can expose security risks, cause memory leaks, or leave services in an undefined state. That ‘s why mastering error handling isn’t just a good practice - it’s essential.
In this guide, we’ll break down the three core error events in Node.js, how and when to use them, and share production-ready patterns you can apply immediately.
🔑 Key Takeaways
unhandledRejection
: ForPromise
errors that lack a.catch()
uncaughtException
: For sync errors that escapetry/catch
uncaughtExceptionMonitor
: For observing fatal errors before they crash- Use graceful shutdowns and always log with context
⚙️ The Node.js Error Event Trilogy
1. unhandledRejection
: Catch the Async Gaps
Triggered when a Promise
rejects and there’s no .catch()
to handle it.
process.on('unhandledRejection', (reason, promise) => {
logger.fatal('Unhandled rejection', { reason });
metrics.increment('errors.unhandledRejection');
if (reason instanceof CriticalError) {
// Perform graceful shutdown
shutdownAndExit(1);
}
});
When to Use:
- Tracking potentially leaked async operations
- Identifying missing error handlers in promise chains
💡 Tip: In production, track these with timestamps or correlation IDs for debugging async chains.
2. uncaughtException
: Your Last Resort
Used to catch sync errors that aren’t handled anywhere else.
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception', { stack: err.stack });
process.exit(1); // Always exit after
});
Critical Guidelines:
- Never continue normal execution
- Always exit with non-zero code
After catching an uncaughtException, never continue normal app logic - the application’s internal state may be unreliable.
3. uncaughtExceptionMonitor
: Observe Without Interfering
This runs before uncaughtException
and is ideal for observability tools like Sentry.
process.on('uncaughtExceptionMonitor', (err) => {
sentry.captureException(err, {
tags: { source: 'uncaughtException' }
});
});
📊 Great for pre-crash diagnostics or forensic logging.
🏗 Framework Integrations
Fastify
example:
// plugins/errorHandler.js
export default async (fastify) => {
process.on('unhandledRejection', (err) => {
fastify.log.fatal(err);
process.exit(1);
});
fastify.setErrorHandler((error, request, reply) => {
request.log.error(error);
reply.status(500).send({ error: 'Internal Server Error' });
});
};
Benefits:
- Combines process-level and route-level handling
- Recommended shutdown workflow:
- Close incoming connections
- Finish ongoing requests
- Exit process
Pair this with Fastify’s lifecycle hooks to ensure a clean shutdown and log everything reliably.
NestJS
example:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = app.get(Logger);
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled Rejection', reason);
});
process.on('uncaughtException', async (err) => {
logger.fatal('Uncaught Exception', err.stack);
await app.close();
process.exitCode = 1; // Allows any final tasks to finish
});
await app.listen(3000);
}
Use
app.close()
to ensure all resources (like DB or messaging) shut down properly.
🔍 Advanced Use Cases
1. Memory Leak Detection
const rejections = new Map();
process.on('unhandledRejection', (reason, promise) => {
rejections.set(promise, Date.now());
});
process.on('rejectionHandled', (promise) => {
const delay = Date.now() - rejections.get(promise);
if (delay > 5000) alertMemoryLeak();
});
2. Distributed Tracing
process.on('unhandledRejection', (reason) => {
const traceId = getCurrentTraceId();
logger.error(`[${traceId}] Unhandled Rejection`, reason);
});
✅ Best Practices Checklist
- ✅ Always log with context (stack trace, request ID, user)
- ✅ Use
process.exitCode
to allow async cleanup - ✅ Combine process-level handlers with local
try/catch
- ✅ Add synthetic tests to verify crash handlers
- ❌ Never ignore
unhandledRejection
warnings - ❌ Don’t resume normal app logic after
uncaughtException
🚀 Final Thoughts
Error handling in Node.js is not just about catching bugs - it’s about building systems that can fail gracefully. By combining low-level process hooks with structured logging, observability tools, and graceful shutdown flows, you can dramatically improve your app’s reliability.
Want your app to stay online at 3AM? Handle your errors like a pro.
Happy coding. 🛠️