process.on('exit')

Registers a synchronous listener that runs just before the process terminates.

Signature
process.on('exit', listener)
  • Added in Node.js v0.1.7

Parameters

ParameterTypeDescription
$eventNamestring | symbolThe name of the event. For this page, the literal string 'exit'.
$listenerFunctionThe callback to add to the end of the listeners array. For 'exit' it is invoked with one argument, code {integer}, taken from process.exitCode or the argument passed to process.exit().

Return value

EventEmitterA reference to process, so calls can be chained.

Errors

Throws ERR_INVALID_ARG_TYPE if listener is not a function. An exception thrown inside an 'exit' listener is not catchable by the program and terminates the process.

process is an EventEmitter, and 'exit' is its last event. It fires either because process.exit() was called or because the event loop ran out of work. The listener receives the final status code, and once every listener returns, the process is gone.

The one rule

Listeners must be synchronous. Nothing you schedule from inside one will ever run — no timer, no setImmediate, no fs.writeFile callback, no await. Registering an async function looks fine and silently does nothing past its first await. If you must persist state on the way out, use a synchronous call such as fs.writeFileSync().

Picking the right event

  • 'exit' — synchronous teardown only. Not emitted on SIGKILL, on a hard crash, or when the process is killed by a signal with no handler.
  • 'beforeExit' — fires when the loop empties and can schedule async work, which keeps the process alive. Not emitted when process.exit() was called explicitly.
  • SIGTERM/SIGINT handlers — where real async shutdown belongs.

Use process.off('exit', fn) to remove a listener, and remember the default cap of 10 listeners before Node.js warns about a leak.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Flush a buffered log or metrics batch to disk synchronously before the process disappears.
  • Print a run summary showing how many records were processed and the final status code.
  • Remove a PID or lock file so the next invocation is not blocked by a stale one.

Frequently asked questions

Why does async code in process.on('exit') not run?

Because Node.js terminates as soon as the last 'exit' listener returns, so anything queued in the event loop — timers, promises, async I/O callbacks — is discarded. Use synchronous calls such as fs.writeFileSync, or move the work into a SIGTERM handler where the loop is still running.

What is the difference between the exit and beforeExit events?

'beforeExit' fires when the event loop empties and may schedule async work that keeps the process alive, while 'exit' is the final synchronous hook and cannot postpone termination. 'beforeExit' is also skipped entirely when process.exit() is called explicitly.

Does process.on('exit') fire when the process is killed?

It fires for SIGTERM or SIGINT only if you have not replaced the default behaviour, and never for SIGKILL, a hard crash, or a power loss. Treat it as best-effort cleanup rather than a durability guarantee.

More examples

Flushing a buffered log on the way out

The buffer normally flushes every 100 entries; the exit listener catches the tail. Swapping appendFileSync for the async fs.appendFile would leave app.log missing the last three lines and the footer, because the callback would be queued into an event loop that never turns again.

Example · javascript
const fs = require('node:fs');

const buffer = [];
function log(msg) {
  buffer.push(`${buffer.length} ${msg}`);
  if (buffer.length >= 100) flush();
}

function flush() {
  if (buffer.length === 0) return;
  // Synchronous on purpose: this is the only kind of write that
  // is guaranteed to complete inside an 'exit' listener.
  fs.appendFileSync('app.log', buffer.join('\n') + '\n');
  buffer.length = 0;
}

process.on('exit', (code) => {
  flush();
  fs.appendFileSync('app.log', `--- exited with ${code} ---\n`);
});

log('started');
log('processed batch A');
log('processed batch B');

// app.log after the run:
// 0 started
// 1 processed batch A
// 2 processed batch B
// --- exited with 0 ---

Where async shutdown actually belongs

The first listener is the mistake almost everyone makes once: it registers cleanly, runs up to the await, and is then discarded when the process terminates, so the pool is never closed and no error appears anywhere. The SIGTERM handler is the correct place, because the event loop is still turning while it awaits.

Example · javascript
import http from 'node:http';

const server = http.createServer((req, res) => res.end('ok'));
server.listen(3000);

// WRONG: nothing after the first await ever runs.
process.on('exit', async () => {
  await pool.end();                 // never resolves
  console.log('pool closed');       // never printed
});

// RIGHT: async teardown belongs in a signal handler.
process.on('SIGTERM', async () => {
  console.log('SIGTERM: draining');
  await new Promise((r) => server.close(r));
  await pool.end();
  console.log('drained; letting the loop empty');
  // No exit() call needed - the loop is now empty.
});

// 'exit' is left for synchronous bookkeeping only.
process.on('exit', (code) => {
  console.log(`shutdown complete, code ${code}`);
});

Exit code and error cause visible in the listener

The uncaughtException handler records a status with process.exitCode rather than calling process.exit(), so the 'exit' listener still runs and can report the outcome. Note that 'exit' is emitted for an uncaught exception but not for a SIGKILL or a native crash, so it is a best-effort hook, never a durability guarantee.

Example · javascript
process.on('exit', (code) => {
  console.log(code === 0 ? 'run succeeded' : `run failed with ${code}`);
});

process.on('uncaughtException', (err) => {
  console.error('uncaught:', err.message);
  process.exitCode = 1;
});

process.on('unhandledRejection', (reason) => {
  console.error('unhandled rejection:', reason);
  process.exitCode = 1;
});

JSON.parse('{ not json');

// Output:
// uncaught: Unexpected token 'n', "{ not json" is not valid JSON
// run failed with 1

Discussion

  • Be the first to comment on this lesson.