Fix: Node.js UnhandledPromiseRejection and uncaughtException — Crashing Server
Quick Answer
How to handle Node.js uncaughtException and unhandledRejection events — graceful shutdown, error logging, async error boundaries, and keeping servers alive safely.
The Error
Node.js crashes with an unhandled exception:
/app/server.js:45
throw new Error('Something went wrong');
^
Error: Something went wrong
at processTicksAndMicrotasks (internal/process/task_queues.js:95:5)
node:internal/process/promises:288
triggerUncaughtException(err, true /* fromPromise */);
^
[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated.
In future versions of Node.js, promise rejections that are not handled will terminate
the Node.js process with a non-zero exit code.Or in Node.js 15+, the process exits immediately:
node:internal/process/promises:246
triggerUncaughtException(err, true /* fromPromise */);
^
Error [ERR_UNHANDLED_REJECTION]: Database connection failed
at Timeout._onTimeout (/app/db.js:23:11)Or an intermittent crash with no error message — the process just exits.
Why This Happens
Node.js has two process-level events for unhandled errors:
uncaughtException— a synchronous throw that wasn’t caught by anytry/catch. In older code, this was common with callbacks.unhandledRejection— a Promise rejection that had no.catch()handler and wasn’t awaited inside atry/catch. This became the dominant source of crashes when async/await was adopted.
The Node.js process exits on unhandledRejection since Node.js 15. In Node.js 14 and earlier, it printed a deprecation warning but continued running — this masked bugs that would have crashed newer versions.
Common causes:
- Async function called without
await— the returned Promise is discarded; any rejection becomes unhandled. - Promise chain missing
.catch()— a.then()chain with no terminal.catch(). setTimeoutorsetIntervalcallback throwing — errors in timer callbacks are uncaught exceptions.- Event emitter throwing — errors emitted as
'error'events with no'error'listener. - Database connection failures — async connection setup that fails after the app starts.
Fix 1: Handle unhandledRejection at the Process Level
Add a global handler to catch any promise rejection that escapes normal error handling:
// At the top of your entry point (index.js / server.js)
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise Rejection:');
console.error('Reason:', reason);
console.error('Promise:', promise);
// Log to error monitoring (Sentry, Datadog, etc.)
errorMonitoring.captureException(reason);
// Graceful shutdown — the process is in an uncertain state
// Don't catch and continue; exit cleanly
server.close(() => {
process.exit(1);
});
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
errorMonitoring.captureException(error);
// Always exit after uncaughtException
// The process state is corrupted — don't try to continue
server.close(() => {
process.exit(1);
});
});Warning: Do NOT use
unhandledRejectionas a catch-all to swallow errors and keep running. The handler should log the error, notify monitoring, and initiate a graceful shutdown. An unhandled rejection means a bug exists — fix the root cause, don’t suppress it.
Fix 2: Always await Async Functions
The most common source of unhandled rejections — calling an async function without await:
// WRONG — rejection is unhandled (promise is discarded)
async function saveUser(data) {
await db.insert('users', data);
}
// Called without await — the returned Promise is ignored
router.post('/users', (req, res) => {
saveUser(req.body); // ← Promise returned and discarded — if it rejects, crash
res.json({ status: 'ok' });
});
// CORRECT — await the async function
router.post('/users', async (req, res, next) => {
try {
await saveUser(req.body); // ← Awaited — rejections are caught
res.json({ status: 'ok' });
} catch (err) {
next(err); // Pass to Express error handler
}
});Promise.all without handling rejections:
// WRONG — if any promise rejects, the rejection is unhandled
Promise.all([
fetchUser(id),
fetchPermissions(id),
fetchSettings(id),
]);
// CORRECT — handle the rejection
Promise.all([
fetchUser(id),
fetchPermissions(id),
fetchSettings(id),
])
.then(([user, permissions, settings]) => {
// process results
})
.catch(err => {
console.error('Failed to load user data:', err);
// Handle the error
});
// Or with async/await
async function loadUserData(id) {
try {
const [user, permissions, settings] = await Promise.all([
fetchUser(id),
fetchPermissions(id),
fetchSettings(id),
]);
return { user, permissions, settings };
} catch (err) {
throw new AppError('Failed to load user data', { cause: err });
}
}Fix 3: Handle Event Emitter Errors
Node.js’s EventEmitter has special handling for the 'error' event. If an 'error' event fires with no listener, Node.js throws an uncaught exception:
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
// WRONG — no 'error' listener
emitter.emit('error', new Error('something failed'));
// → Uncaught 'Error' [ERR_UNHANDLED_ERROR]: Unhandled error. (something failed)
// Process crashes
// CORRECT — always add an 'error' listener to EventEmitters you use
emitter.on('error', (err) => {
console.error('EventEmitter error:', err);
});Streams (which extend EventEmitter):
const fs = require('fs');
// WRONG — no error handler on readable stream
const stream = fs.createReadStream('/nonexistent/file.txt');
stream.on('data', chunk => console.log(chunk));
// If file doesn't exist → uncaught error → crash
// CORRECT — always handle stream errors
const stream = fs.createReadStream('/path/to/file.txt');
stream.on('data', chunk => process.stdout.write(chunk));
stream.on('error', err => {
console.error('Stream error:', err.message);
// Handle appropriately
});
stream.on('end', () => console.log('Done'));HTTP server and socket errors:
const http = require('http');
const server = http.createServer(app);
// Handle server-level errors (port already in use, etc.)
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} is already in use`);
process.exit(1);
}
throw err;
});
// Handle errors on individual socket connections
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});Fix 4: Implement Graceful Shutdown
A process restart (after a crash) is unavoidable — implement graceful shutdown to minimize disruption:
const express = require('express');
const app = express();
const server = app.listen(3000);
let isShuttingDown = false;
function gracefulShutdown(signal) {
console.log(`Received ${signal} — starting graceful shutdown`);
isShuttingDown = true;
// Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed');
try {
// Close database connections
await db.pool.end();
console.log('Database connections closed');
// Close other resources (Redis, message queues, etc.)
await redis.quit();
console.log('Redis connection closed');
console.log('Graceful shutdown complete');
process.exit(0);
} catch (err) {
console.error('Error during shutdown:', err);
process.exit(1);
}
});
// Force exit if graceful shutdown takes too long
setTimeout(() => {
console.error('Graceful shutdown timed out — forcing exit');
process.exit(1);
}, 30000); // 30 second timeout
}
// Reject new requests during shutdown
app.use((req, res, next) => {
if (isShuttingDown) {
res.setHeader('Connection', 'close');
return res.status(503).json({ error: 'Server is shutting down' });
}
next();
});
// Handle shutdown signals
process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Kubernetes, Docker stop
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C
// Handle unhandled errors
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
gracefulShutdown('unhandledRejection');
});Fix 5: Use a Process Manager to Auto-Restart
Since crashes can happen even with good error handling, run Node.js under a process manager that automatically restarts the process:
PM2:
npm install -g pm2
# Start with auto-restart
pm2 start server.js --name "api-server"
pm2 start server.js --name "api-server" --max-restarts 10 --restart-delay 5000
# Ecosystem file for production// ecosystem.config.js
module.exports = {
apps: [{
name: 'api-server',
script: 'server.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster',
max_restarts: 10, // Max restarts in restart_delay period
restart_delay: 5000, // Wait 5s before restart
max_memory_restart: '500M', // Restart if memory exceeds 500MB
env: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: '/var/log/pm2/api-error.log',
out_file: '/var/log/pm2/api-out.log',
}],
};Docker with restart policy:
# docker-compose.yml
services:
api:
image: my-api:latest
restart: unless-stopped # Always restart except on explicit stop
# Or for production:
# restart: always
# Kubernetes handles restarts automatically via Pod restart policyFix 6: Use Domain for Legacy Code (Deprecated but Informational)
Node.js had domain for grouping async operations and handling their errors — it was deprecated and should not be used in new code. The modern equivalent is proper async/await error handling:
// LEGACY — don't use in new code
const domain = require('domain');
const d = domain.create();
d.on('error', (err) => {
console.error('Domain caught:', err);
});
d.run(() => {
// Async operations inside the domain have errors caught by d.on('error')
setTimeout(() => {
throw new Error('Caught by domain');
}, 100);
});
// MODERN equivalent — proper try/catch with async/await
async function runOperation() {
try {
await asyncOperation();
} catch (err) {
console.error('Caught:', err);
}
}Fix 7: Structured Error Logging
Make unhandled errors visible and actionable with structured logging:
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.json(),
),
transports: [
new transports.Console(),
new transports.File({ filename: 'error.log', level: 'error' }),
],
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Promise Rejection', {
reason: reason instanceof Error ? {
message: reason.message,
stack: reason.stack,
name: reason.name,
} : reason,
timestamp: new Date().toISOString(),
});
// Initiate graceful shutdown
gracefulShutdown('unhandledRejection');
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception', {
message: error.message,
stack: error.stack,
name: error.name,
});
gracefulShutdown('uncaughtException');
});Error monitoring with Sentry:
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
});
// Sentry automatically captures unhandledRejection and uncaughtException
// when initialized — no manual .on() needed for basic capture
// For custom context:
process.on('unhandledRejection', (reason) => {
Sentry.captureException(reason);
gracefulShutdown('unhandledRejection');
});Still Not Working?
Find the source of the rejection — add a --trace-warnings flag to see where the unhandled rejection originated:
node --trace-warnings --trace-uncaught server.js
# Shows a full stack trace including where the Promise was createdAsync event handlers — event handlers that are async but not awaited cause silent unhandled rejections:
// WRONG — EventEmitter doesn't await async listeners
emitter.on('data', async (event) => {
await processEvent(event); // If this throws, rejection is unhandled
});
// CORRECT — catch errors inside the async listener
emitter.on('data', async (event) => {
try {
await processEvent(event);
} catch (err) {
emitter.emit('error', err); // Re-emit as error event
}
});Promise.allSettled instead of Promise.all for non-critical operations:
// Promise.all rejects on first failure — other promises continue untracked
// Promise.allSettled waits for all, never rejects, reports each result
const results = await Promise.allSettled([
sendWelcomeEmail(user),
updateAnalytics(user),
notifyAdmin(user),
]);
for (const result of results) {
if (result.status === 'rejected') {
console.error('Non-critical operation failed:', result.reason);
}
}For related Node.js issues, see Fix: Express Async Error Not Being Caught and Fix: Node.js Heap Out of Memory.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Express Async Error Not Being Caught — Unhandled Promise Rejection
How to fix Express async route handlers not passing errors to the error middleware — wrapping async routes, using express-async-errors, global error handlers, and Node.js unhandledRejection events.
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.