Uncaught Exceptions
Every error-handling strategy has a last line of defence. Even with careful try/catch and centralised middleware, a synchronous throw that escapes all handlers — a bug in a third-party library, an error in startup code, a deep callback you forgot about — would normally crash the process with an ugly stack trace and no cleanup. process.on('uncaughtException') lets you intercept that final throw, log it, and shut down gracefully rather than abruptly. But it is not a recovery mechanism — it's a controlled-crash mechanism.
What triggers it
// A throw that escapes ALL try/catch in synchronous code:
setTimeout(() => {
throw new Error('Something exploded') // no wrapping try/catch
}, 1000)
// A missing property deep in a callback:
EventEmitter.on('data', (chunk) => {
process(chunk.nested.value) // TypeError if nested is undefined
})The handler
process.on('uncaughtException', (err, origin) => {
// 'origin' is 'uncaughtException' or 'unhandledRejection' (Node 12+)
logger.fatal({ err, origin }, 'Uncaught exception — process will exit')
// Flush any buffered logs, notify alerting:
// e.g. Sentry.captureException(err)
// IMPORTANT: exit after logging — do NOT try to continue:
process.exit(1)
})What you can do in the handler
Log the full error — message, stack, and any request context you can access.
Notify alerting — Sentry, PagerDuty, Datadog, etc.
Flush log buffers — some loggers are asynchronous; give them a moment.
Call
process.exit(1)— required; do not omit or defer it.Do NOT — serve more requests, run async code that might throw again, or try to "recover."
The wrong way — catching to continue
// ❌ DANGEROUS — the process is in an unknown state after this:
process.on('uncaughtException', (err) => {
console.error('Caught:', err.message)
// no process.exit() — the server keeps running in a corrupt state
})Don't use it as a substitute for proper error handling
In production: pair with a supervisor
# PM2 restarts the process automatically on crash: pm2 start app.js --name api # Kubernetes: restartPolicy: Always (the default) does the same. # systemd: Restart=always # The pattern: process crashes cleanly (exit 1) → supervisor restarts it # → new process starts in a known-good state.