UNPKG

catch-exit

Version:

Catch Node.js exit conditions, including errors and unhandled rejections.

148 lines (147 loc) 4.83 kB
import { signalsByName } from 'human-signals'; import { createHook } from 'node:async_hooks'; import { writeSync } from 'node:fs'; const callbacks = new Set(); /** * Add a callback function to be called upon process exit or death. * * @category Main * @returns The callback itself for chaining purposes. */ export function addExitCallback( /** * Typed to block async functions. Async functions will not work for 'exit' events, triggered * from process.exit(), but will work with other events this catches. If you wish to perform * async functions have this callback call an async function but remember it won't be awaited if * the signal is 'exit'. */ callback) { /** Only setup the exit handling once a callback has actually been added. */ setupProcessExitHandling(); callbacks.add(callback); return callback; } /** * Remove the given callback function from the set of previously added exit callbacks. * * @category Main * @returns `true` if the callback was found and removed. `false` otherwise. */ export function removeExitCallback( /** The exact callback (by reference) to remove, previously added by {@link addExitCallback}. */ callback) { return callbacks.delete(callback); } /** * Signals listened to by this package. * * @category Internal */ export const interceptedSignals = [ 'SIGHUP', /** Catches ctrl+c event. */ 'SIGINT', /** Catches "kill pid". */ 'SIGTERM', 'SIGQUIT', ]; /** * Signals that may be passed to {@link ExitCallback} as its `signal` argument. * `'unhandledRejection'` is not a part of this because those are all turned into * `'uncaughtException'` errors. * * @category Internal */ export const catchSignalStrings = [ ...interceptedSignals, 'exit', 'uncaughtException', ]; function logError(value) { writeSync(2, value); } /** I'm not sure what all the different async types mean but at least these don't seem to matter. */ const ignoredAsyncTypes = [ 'TTYWRAP', 'SIGNALWRAP', 'PIPEWRAP', ]; let asyncWarningAlreadyLogged = false; const asyncHook = createHook({ init(id, type) { if (!ignoredAsyncTypes.includes(type) && !asyncWarningAlreadyLogged) { asyncWarningAlreadyLogged = true; logError("\nWarning: an async 'process.exit' callback was used. Async exit callbacks will not run to completion because 'process.exit' does not complete async tasks.\nSee https://www.npmjs.com/package/catch-exit#async-warning for details.\n"); } }, }); /** * This is used to prevent attaching signal listeners multiple times (which isn't necessary and * actually causes issues). */ let alreadySetup = false; /** * This is used to prevent double clean up (since the process.exit in exitHandler gets caught the * first time, firing exitHandler again). */ let alreadyExiting = false; function stringifyError(error) { if (error instanceof Error) { return (error.stack || error.toString()) + '\n'; } else { return String(error); } } function setupProcessExitHandling() { if (alreadySetup) { return; } alreadySetup = true; function exitHandler(signal, exitCode, inputError) { if (!alreadyExiting) { alreadyExiting = true; try { /** Only the exit signal has async issues. */ if (signal === 'exit') { asyncHook.enable(); } callbacks.forEach((callback) => callback(signal, exitCode, inputError)); asyncHook.disable(); } catch (callbackError) { /** * 7 here means there was an error in the exit handler, which there was if we got to * this point. */ exitWithError(callbackError, 7); } if (inputError instanceof Error) { exitWithError(inputError, exitCode); } else { process.exit(exitCode); } } } /** Prevents all exit codes from going straight to `7` when they shouldn't be. */ function exitWithError(error, code) { logError(stringifyError(error)); process.exit(code); } interceptedSignals.forEach((signal) => process.on(signal, () => { const signalNumber = signalsByName[signal].number; exitHandler(signal, 128 + signalNumber); })); process.on('exit', (code) => { exitHandler('exit', code); }); process.on('unhandledRejection', (reason) => { const error = reason instanceof Error ? reason : new Error(String(reason)); error.name = 'UnhandledRejection'; throw error; }); process.on('uncaughtException', (error) => { exitHandler('uncaughtException', 1, error); }); }