closeout
Coming from…

Coming from signal-exit

A signal-exit alternative with zero dependencies: closeout's onExit runs once on every exit path with a bounded deadline, and reports as --json or an agent event. No signal-exit drop-in yet — its compatibility row is planned, not graded.

closeout is a signal-exit alternative: exit handlers that run exactly once on every path, terminal restore, and a bounded deadline so shutdown cannot hang. It is not yet a signal-exit drop-in, and this page says why.

Migrate from signal-exit: rename the callback's arguments

There is no closeout/signal-exit subpath. Both packages export onExit, and both return the function that unregisters the handler, but the callback is handed a different shape — signal-exit passes (code, signal), closeout passes one record:

- import { onExit } from 'signal-exit';
- const off = onExit((code, signal) => {
+ import { onExit } from 'closeout';
+ const off = onExit(({ path, code, signal, error }) => {
    releaseTheLock();
  });

path is 'exit' | 'beforeExit' | 'signal' | 'uncaught' | 'rejection', and error is what was thrown or rejected on the two paths that have one.

The two neighbours in this layer do have one-import paths, each graded by its incumbent's own suite:

- import exitHook, { asyncExitHook, gracefulExit } from 'exit-hook';
- import restoreCursor from 'restore-cursor';
+ import exitHook, { asyncExitHook, gracefulExit } from 'closeout/exit-hook';
+ import restoreCursor from 'closeout/restore-cursor';

Is closeout compatible with signal-exit?

There is no graded signal-exit row. On the Compatibility page, which npm run compat:page generates from the oracle's last run, signal-exit is listed as planned: its suite is vendored and runs, but the control does not yet clear its own reference, and a control that cannot must not publish a rate. Until it is graded, closeout does not claim signal-exit compatibility. The rows that are graded:

closeoutcontrol
closeout/exit-hook21 / 2121 / 21
closeout/restore-cursor6 / 66 / 6

What you gain over signal-exit

  • Exactly once, on every door. A normal exit, an emptied event loop, Ctrl-C, SIGTERM, SIGHUP, SIGQUIT, an uncaught throw and an unhandled rejection all reach the same handler, once, even when two fire at the same moment.
  • Shutdown cannot hang. A bounded deadline — DEFAULT_DEADLINE is 2000 ms — ends a shutdown whose handlers do not return, and the result names the ones that had not.
  • Phases, not registration order. Handlers run in flush, release, restore order, so the cursor comes back after the file is flushed, whichever registered first.
  • One record, three readers. The record your handler receives is what reportToJson() and reportToEvent() project, so a --json line and an agent event cannot disagree with what the handler was told.
  • Terminal restore is built in. hideCursor(stream) registers its own restore.

SIGKILL is not deliverable to a listener, by design, and closeout does not claim it.

When to switch from signal-exit

  • Ctrl-C leaves a hidden cursor, a half-written file or a held lock.
  • A shutdown handler that never returns has hung your CLI or your CI job.

The API, the phases and the deadline are on closeout.

On this page