Zero-dependency non-invasive fatal error guard for Node.js CLIs.
Capture uncaught exceptions, unhandled rejections, and non-zero exits — route a structured 4-field payload to any external handler.
npm i @misaka-net/fatal-guard
FATAL_HANDLER=/usr/bin/logger node -r @misaka-net/fatal-guard/register ./app.jsOne env var. No source code changes.
# Wrap any Node.js CLI — automatically detects crashes via stderr + exit code
FATAL_HANDLER=/usr/bin/logger npx @misaka-net/fatal-guard -- node app.js
# Works with any CLI tool — Vite, E2B, OpenClaw, tsdown, etc.
FATAL_HANDLER=/usr/bin/logger npx @misaka-net/fatal-guard -- ./node_modules/.bin/vite buildnpm i @misaka-net/fatal-guard
FATAL_HANDLER=/usr/bin/logger node -r @misaka-net/fatal-guard/register ./app.jsBoth modes produce identical payloads in syslog on crash (7 fields since v0.3):
{
"schemaVersion": 1,
"reason": "uncaught_exception",
"timestamp": "2026-07-24T...",
"pid": 12345,
"errorName": "TypeError",
"message": "Cannot read properties of undefined",
"stackSnippet": "TypeError: Cannot read properties...\n at app.js:42:3"
}All diagnostic fields (errorName, message, stackSnippet) are automatically redacted to prevent secret leakage.
# 1. Install
npm i @misaka-net/fatal-guard
# 2. Run any Node.js CLI with the guard preloaded
FATAL_HANDLER=/usr/bin/logger node -r @misaka-net/fatal-guard/register ./your-cli.js
# 3. Trigger a fatal error — watch syslog light up
# (uncaught exceptions, unhandled rejections, non-zero exits all captured)Real syslog output after an uncaught exception:
Jun 19 08:38:26 hostname logger[180739]:
{"schemaVersion":1,"reason":"uncaught_exception",
"timestamp":"2026-06-19T08:38:26.091Z","pid":180739}
Jun 19 08:38:26 hostname logger[180739]:
{"schemaVersion":1,"reason":"exit_code",
"timestamp":"2026-06-19T08:38:26.096Z","pid":180739}
register.js hooks into three process-level fatal signals:
| Signal | Trigger | Behavior |
|---|---|---|
uncaughtException |
Synchronous throw not caught by any try/catch | Fires handler, prints to stderr, exits with code 1 |
unhandledRejection |
Async promise rejection with no .catch() |
Fires handler, prints warning |
exit_code / SIGTERM / SIGINT |
Non-zero exit or termination signal | Fires handler |
On each signal, the handler executable is spawned with a single JSON argv argument:
{
"schemaVersion": 1,
"reason": "uncaught_exception",
"timestamp": "2026-06-19T04:20:00.000Z",
"pid": 12345
}4 fields only. No stack traces, no environment variables, no secrets. Designed to be redacted by default — the handler decides what to enrich.
| Handler | Command | Effect |
|---|---|---|
| syslog | FATAL_HANDLER=/usr/bin/logger |
Writes to systemd journal / syslog |
| HTTP webhook | FATAL_HANDLER=/usr/bin/curl |
Receives JSON as argv[1] — pair with a small wrapper script for HTTP POST |
| Custom script | FATAL_HANDLER=/opt/alert-to-slack.sh |
Write your own alert pipeline |
| Principle | Implementation |
|---|---|
| Zero dependencies | require('node:child_process') only |
| Non-blocking | spawn + detached: true + unref() — handler never blocks shutdown |
| Injection-safe | shell: false — no shell interpretation of env var or payload |
| Fire-and-forget | Handler failure is silently swallowed — process continues |
| Redacted by default | 4 fields only — no stack, no env vars, no secrets |
Alerts #46, #47, #48 were dismissed as "won't fix" with the following rationale:
shell: falseis used in allspawn()calls — no shell interpretation- Command/handler paths come from user-controlled inputs (CLI args or env vars like
FATAL_HANDLER), not from untrusted external sources - On Windows,
.cmd/.batfiles require routing throughComSpec(cmd.exe) — this is a Windows platform requirement, not a security issue - There is no external attacker who can inject commands — the user explicitly provides the command to wrap
The spawn-command.js module routes only .cmd/.bat files through ComSpec on Windows. Normal executables are spawned directly without shell involvement. See packages/fatal-guard/src/lib/spawn-command.js for the implementation.
const { buildPayload, runHandler } = require('@misaka-net/fatal-guard');
buildPayload('uncaught_exception');
// → '{"schemaVersion":1,"reason":"uncaught_exception","timestamp":"...","pid":12345}'
runHandler('uncaught_exception');
// → spawns FATAL_HANDLER with payload as argv[1], or no-op if FATAL_HANDLER is unset- GitHub
- npm:
@misaka-net/fatal-guard