How to exit from a Node.js program
By Flavio
Published · Updated
Tagged: Node.js
Learn how to exit a Node.js program: process.exit vs exitCode, SIGINT and SIGTERM, graceful shutdown with a timeout, and the exit events.
There are various ways to terminate a Node.js application.
When running a program in the console you can close it with ctrl-C, but what I want to discuss here is programmatically exiting.
Let’s start with the most drastic one, and see why you’re better off not using it.
The process core module provides a handy method that allows you to programmatically exit from a Node.js program: process.exit().
When Node.js runs this line, the process is immediately forced to terminate.
This means that any callback that’s pending, any network request still being sent, any filesystem access, or processes writing to stdout or stderr - all is going to be ungracefully terminated right away.
If this is fine for you, you can pass an integer that signals the operating system the exit code:
process.exit(1)
By default the exit code is 0, which means success. Different exit codes have different meaning, which you might want to use in your own system to have the program communicate to other programs.
You can read more on exit codes at https://nodejs.org/api/process.html#process_exit_codes
Prefer process.exitCode over process.exit()
You can also set the process.exitCode property:
process.exitCode = 1
and when the program will later end, Node will return that exit code.
A program will gracefully exit when all the processing is done.
Node keeps running until the event loop is empty. Pending writes to stdout and stderr get to finish. Then the process exits with that code.
process.exit() does not wait. It asks Node to stop as soon as the 'exit' listeners return. A console.log() right before process.exit() can get cut off, because those writes are sometimes async.
My advice is to set process.exitCode and let the process end on its own. Call process.exit() only when you must force a stop, like a shutdown that hung.
If the program hit a real error, throwing and letting Node crash is safer than process.exit(). Node prints the stack and exits with code 1.
Close a server on SIGTERM and SIGINT
Many times with Node we start servers, like this HTTP server:
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hi!')
})
app.listen(3000, () => console.log('Server ready'))
This program is never going to end. If you call process.exit(), any currently pending or running request is going to be aborted. This is not nice.
In this case you need to send the command a SIGTERM signal, and handle that with the process signal handler:
Note:
processdoes not require a “require”, it’s automatically available.
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hi!')
})
const server = app.listen(3000, () => console.log('Server ready'))
process.on('SIGTERM', () => {
server.close(() => {
console.log('Process terminated')
})
})
What are signals? Signals are a POSIX intercommunication system: a notification sent to a process in order to notify it of an event that occurred.
SIGKILL is the signal that tells a process to immediately terminate, and would ideally act like process.exit(). You cannot listen for SIGKILL. The OS kills the process. No cleanup.
SIGTERM is the signal that tells a process to gracefully terminate. It is the signal that’s sent from process managers like systemd or supervisord, and from docker stop, among others.
SIGINT is what the terminal sends when you press Ctrl-C. If you only handle SIGTERM, Ctrl-C still uses Node’s default: it resets the terminal and exits with code 130 (128 + 2). The server never gets server.close().
Handle both with the same function.
A graceful shutdown with a timeout
server.close() stops accepting new connections and waits for the open ones to finish. A keep-alive socket or a stuck query can make that wait never end.
Close the HTTP server, close the other resources you opened, and force an exit if that work hangs.
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hi!')
})
const server = app.listen(3000, () => console.log('Server ready'))
let closing = false
function shutdown(signal) {
if (closing) {
return
}
closing = true
console.log(`Got ${signal}, closing`)
server.close(() => {
console.log('HTTP server closed')
})
// close other resources here, for example a DB pool:
// pool.end()
setTimeout(() => {
console.error('Close timed out, forcing exit')
process.exit(1)
}, 10000).unref()
}
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))
unref() lets the process exit if server.close() finishes before the 10 seconds. The timer is only a fallback.
If you have a database pool, call pool.end() next to server.close(). Same idea for a Redis client or an open file. Once every handle is closed and the event loop is empty, Node exits. You can set process.exitCode in the close callback if something failed.
You can send this signal from inside the program, in another function:
process.kill(process.pid, 'SIGTERM')
Or from another Node.js running program, or any other app running in your system that knows the PID of the process you want to terminate.
The 'exit' and 'beforeExit' events
Node also emits two events around shutdown.
'beforeExit' runs when the event loop is empty and Node is about to exit on its own. You can start more async work from here, and that keeps the process alive. It does not run if you called process.exit(), and it does not run for an uncaught exception.
'exit' runs when Node is actually leaving. The callback gets the exit code. You can only do synchronous work. A setTimeout() inside 'exit' never fires. The process is gone after the listeners return.
process.on('beforeExit', (code) => {
console.log(`beforeExit, code ${code}`)
})
process.on('exit', (code) => {
console.log(`exit, code ${code}`)
})
Use 'exit' for a last sync log line. Do not close a server or a database there. That work is async, and it will not finish.
Exiting on uncaught errors
If an exception is not caught, Node prints the stack to stderr and exits with code 1. That overrides a process.exitCode you set earlier.
An unhandled promise rejection does the same thing by default. It is raised as an uncaught exception.
You can listen for 'uncaughtException' or 'unhandledRejection'. Use those only to log and do synchronous cleanup, then let the process die. The app is in an unknown state. Do not keep serving requests after that.
process.on('uncaughtException', (err) => {
console.error('Uncaught exception', err)
process.exit(1)
})
A process manager can then start a fresh process. Trying to resume inside 'uncaughtException' is a last resort, and Node’s docs warn against it.
Want me to talk about your product? You can sponsor this site.
Related posts about node: