uncaught ─┐
rejected ─┤ ╭───────────────╮ errors.jsonl
capture() ─┴─▶ │ ▓ flight log ▓ │ ─▶ {"ts":…,"kind":"uncaught",…}
╰───────────────╯ {"ts":…,"kind":"manual",…}
survives the crash {"ts":…,"kind":"unhandledRejection",…}
· readable anytime
flightlog
A flight recorder for your app. Catches what would otherwise vanish — uncaught exceptions, unhandled rejections, and errors you hand it — and writes each as one JSON line to a local file you can read anytime, even on a healthy app. ~130 lines of code, zero production dependencies. The JSONL is the interface.
flightlog is the ~20-line global error net — packaged once, audited, so it wires into every app the same way. install() once: it registers the handlers, runs a boot-time writability check, and hands you capture() (async) plus captureSync() (for log-then-exit in short-lived processes). Every uncaught exception, unhandled rejection, and value you pass to capture() becomes one normalized JSONL line.
Same build ethos as the bare suite — embed it, don't run it; zero deps; no daemon, no SaaS, no telemetry — but flightlog is substrate for your own apps, not agent infrastructure. Closest sibling: mailproof.
It owns exactly one layer: catch → normalize → append. It is not a general logger (no info/warn levels — that's pino/winston), not aggregation or dedup (that's jq when you need it), not breadcrumbs or auto-captured context (the surveillance payload a privacy tool refuses), and ships no UI, server, or reader — the JSONL is the whole read surface. It never harvests context: it logs only what you hand it.
npm install flightlog
ESM-only. ESM consumers (import) need Node.js >= 18. CommonJS consumers (require) need Node >= 22.12 (require(esm)) or await import("flightlog") — see the integration guide. Zero production dependencies (vanilla + node:fs). Ships TypeScript types generated from JSDoc — import { install } from "flightlog" gives you autocomplete out of the box, no @types package needed.
import { install } from "flightlog";
const { capture, captureSync } = install({
file: "/var/log/myapp/errors.jsonl", // sink; omit → stderr
context: { app: "myapp", release: "v1.4.2" }, // static, you choose — never auto-harvested
exitOnUncaught: true, // default; false = log-and-stay-alive (CLI/desktop)
exitOnRejection: false, // default; true = fatal rejections (short-lived procs)
bootCheck: true, // default; false = warn (don't throw) if sink unwritable
maxBytes: 5_000_000, // default 5 MB; 0 disables rotation
});
// Operational errors you catch at a boundary — the request fails, the server stays up:
try { await risky(); } catch (err) { capture(err, { where: "checkout", userId }); }
// Short-lived process (CLI/cron/pipe) that logs-then-exits — use the sync sibling:
try { main(); } catch (err) { captureSync(err, { where: "receive" }); process.exit(1); }That's the whole wiring. Uncaught exceptions log synchronously then exit(1) (your supervisor restarts clean); unhandled rejections log and stay alive (set exitOnRejection: true to make them fatal); capture() is fire-and-forget while captureSync() flushes before you exit; neither throws.
Wiring it into a real app? Hand your AI assistant the integration guide and describe what you want:
Read flightlog.context.md from node_modules/flightlog/flightlog.context.md,
then wire flightlog into my app. Here's my setup: <describe app, log path, context>.
That file has every option, the full record shape, the crash policy, the rejection gotcha, the threat model, and the jq / tail reading recipes.
One flat JSON object per error — kind is how it was caught, everything after stack is context you supplied:
{"ts":"2026-05-31T12:00:00.000Z","kind":"uncaught","name":"TypeError","message":"x is not a function","stack":"…","app":"myapp","release":"v1.4.2"}| What | Behavior |
|---|---|
| uncaught exception | log synchronously → exit(1) (unless exitOnUncaught: false) so a supervisor restarts a clean process |
| unhandled rejection | log only — intentionally suppresses Node's default crash-on-rejection; a stray rejection shouldn't down a server. Set exitOnRejection: true to log (sync) then exit(1) for short-lived processes |
manual capture(err, extra?) |
normalize + append { ...context, ...extra }; async/fire-and-forget; never throws, never exits |
manual captureSync(err, extra?) |
same record, written synchronously so it survives a process.exit() right after — for log-then-exit in short-lived processes. Returns { ok, errno? } so a per-invocation process can tell "logged" from "silently dropped" and set its exit code; ignorable otherwise |
non-Error throws (throw "x", objects, null) |
described faithfully, given a stack synthesized at the call site — not flightlog's internals |
| disk growth | size cap + rotation: at maxBytes the file rolls to .1 (current + one previous, bounded ~2×). 0 disables |
| broken sink (perms / read-only / full disk) | swallowed — never crashes your app — and surfaced once to stderr with the errno, reset on recovery |
| boot-time check | install() probes the sink and, by default, throws on a bad path — fail loud at startup. bootCheck: false warns-once instead, for short-lived/per-invocation processes (cron, mail pipes) where a fatal boot would take down the real work |
| file perms | created 0600 (owner-only) so error data isn't world-readable on a shared host |
34 tests pass on CI (Node 22): unit (normalize on every throw shape), integration (rotation, self-failure warn-once, boot check, 0600 perms), subprocess (the crash policy, each kind, captureSync surviving an immediate exit, exitOnRejection, and bootCheck: false surviving an unwritable sink end to end), and a packed-tarball E2E that extracts the real artifact and imports it by bare specifier.
| Integration Guide | The complete adopter contract — options, API, record shape, gotchas, threat model. Hand it to your AI assistant. |
| Examples | read.js — zero-dep reader recipes (filter by kind/where/proc, tail) that print their jq equivalents; ship.js — a consent-gated uploader for shipping the JSONL back to yourself. The layers flightlog won't do. Both (repo-only) — copy and adapt. |
| PRD | Locked decisions + why, success criteria, the refusals, build order. (repo-only) |
| CHANGELOG | keep-a-changelog; an entry every release. |
Apache 2.0. See LICENSE.