NodeJSProcess Events & Signals

Process Events & Signals

The process object is an EventEmitter, so you can subscribe to lifecycle moments and operating-system signals. This is how production services implement graceful shutdown, catch otherwise-fatal errors, and clean up resources before they vanish. Getting these handlers right is the difference between a deploy that drops live connections and one that drains them cleanly.

The lifecycle events

Event

Fires when

Async allowed?

exit

The process is about to exit — last chance

No — sync only

beforeExit

The loop emptied with no pending work

Yes — you may schedule more

uncaughtException

An error bubbled all the way up uncaught

Log & exit only

unhandledRejection

A promise rejected with no .catch

Log & exit only

warning

Node emits a runtime warning (e.g. leak)

Yes

SIGINT

Ctrl+C in the terminal

Yes

SIGTERM

A polite "please stop" (Docker/K8s/kill)

Yes

exit vs beforeExit — a crucial difference
The `exit` handler runs in a world where the event loop has **already stopped** — any `setTimeout`, promise, or I/O you start there is silently ignored. Only synchronous code runs. `beforeExit`, by contrast, fires *because* the loop ran dry, and you *can* schedule new async work there (which is why it can fire more than once, and never fires on `process.exit()` or a fatal error).
Graceful shutdown

When an orchestrator stops your container it sends SIGTERM, then waits a grace period (often 30s) before SIGKILL. A good service stops accepting new work, finishes in-flight requests, closes the DB, then exits — instead of dropping connections mid-flight:

graceful-shutdown.js

JS
let shuttingDown = false

function shutdown(signal) {
  if (shuttingDown) return        // ignore a second Ctrl+C
  shuttingDown = true
  console.log(`Received ${signal}, shutting down gracefully...`)

  server.close(() => {            // 1. stop accepting new connections
    db.close()                    // 2. release resources
    console.log('Closed cleanly')
    process.exit(0)               // 3. success
  })

  // Safety net: force-exit if cleanup hangs past the grace period
  setTimeout(() => {
    console.error('Cleanup timed out, forcing exit')
    process.exit(1)
  }, 10_000).unref()
}

process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))
What is .unref()?
Calling `.unref()` on a timer tells Node "this timer alone should not keep the process alive." So the safety-net timeout will *not* hold the process open if cleanup finishes early — but it *will* fire if shutdown stalls. Without `.unref()`, the 10-second timer would itself delay every clean exit.
server.close() does not kill keep-alive sockets
`server.close()` stops accepting *new* connections but waits for existing ones to go idle — and HTTP keep-alive sockets can linger. This is exactly why you need the force-exit safety net. In Node 18.2+ you can also call `server.closeAllConnections()` / `closeIdleConnections()` to hurry it along.
Last-resort error handlers

JS
process.on('uncaughtException', (err, origin) => {
  console.error('Uncaught exception:', err, 'from', origin)
  // Log, flush, then exit — the process is in an unknown state.
  process.exit(1)
})

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason)
  process.exit(1)
})
These are smoke detectors, not error handling
`uncaughtException` and `unhandledRejection` mean a bug slipped past your normal `try/catch` and `.catch()`. **Log and exit** — do not try to "resume" as if nothing happened; the heap, open handles, and in-flight state may be corrupt. Let your process manager (systemd, PM2, Kubernetes) restart a fresh instance. Since Node 15, an unhandled rejection crashes the process by default anyway.
Common signals

Signal

Typical source

Catchable?

SIGINT

Ctrl+C at the terminal

Yes

SIGTERM

kill, Docker stop, K8s pod termination

Yes

SIGHUP

Terminal closed; often used for "reload config"

Yes

SIGKILL

kill -9, OOM killer

No — instant death

SIGSTOP

Job control (pause)

No

Bash
kill -SIGTERM 48213     # polite stop — your handler runs
kill -SIGKILL 48213     # forceful — no handler, no cleanup, gone
You cannot trap SIGKILL or SIGSTOP
The OS handles `SIGKILL` and `SIGSTOP` directly — your process never sees them, so no cleanup runs. This is deliberate: it guarantees an operator can always stop a runaway process. Design so an abrupt kill is survivable (idempotent writes, transactions), because it *will* happen.
Windows is different
Signals on Windows
POSIX signals do not exist natively on Windows. Node emulates a few: `SIGINT` works for Ctrl+C, and `SIGTERM`/`SIGKILL` can be *sent* to terminate a process, but most others are unsupported. For cross-platform shutdown, rely on `SIGINT` and `SIGTERM` only.
Next
Move on to a tool you will use in every file: [The console Module](/nodejs/console-module).