Enrich thrown errors with git context — who wrote the line, what changed recently, what the code around it looks like — and print it to your terminal or post it to Slack. No account, no SaaS, no data leaves your machine unless you configure a Slack webhook yourself.
The anti-Sentry. Zero runtime dependencies — the core package shells
out to the git binary directly instead of pulling in a wrapper library.
npm install gitfaultRequires Node 18+ and a git repository (gitfault reads git blame / git log from the file that threw — it isn't useful outside a repo).
import { attachGlobalHandlers } from 'gitfault/attach';
attachGlobalHandlers();That's it. Any uncaught exception or unhandled rejection now prints an enriched report to stderr before the process exits, e.g.:
Error: kaboom
at /repo/src/orders.js:42
Blame:
Jane Doe · a1b2c3d · fix race condition in checkout
Source:
37 | function processOrder(order) {
38 | validate(order);
39 |
> 42 | throw new Error('kaboom');
41 |
42 | }
Recent commits:
a1b2c3d fix race condition in checkout
9f8e7d6 add order validation
import { gitfaultHandler } from 'gitfault/attach';
// Express
app.use(gitfaultHandler());
// Fastify
fastify.setErrorHandler(gitfaultHandler());The same handler works with both frameworks — it detects which one called
it (Express passes a next callback, Fastify doesn't) and either forwards
the error unchanged via next(err) or sends it via reply.send(err).
Either way, the original error is never swallowed or replaced —
enrichment is purely observational.
Two ship out of the box: a terminal sink (default) and a Slack sink.
import { attachGlobalHandlers, createSlackSink, terminalSink } from 'gitfault/attach';
attachGlobalHandlers({
sinks: [terminalSink, createSlackSink({ webhookUrl: process.env.SLACK_WEBHOOK_URL })],
});createSlackSink posts a Block Kit message (error, file:line, blame,
source snippet, and a commit link if a git remote is configured) to a
Slack incoming webhook. It never throws or blocks the caller — failures
are logged locally with console.error and swallowed. Repeated errors
from the same throw site are rate-limited to one Slack message per minute
by default.
createSlackSink({
webhookUrl: 'https://hooks.slack.com/services/...', // or set GITFAULT_SLACK_WEBHOOK_URL
rateLimitMs: 60_000, // 0 disables rate limiting
});Write your own sink by matching the shape (enriched: EnrichedError) => void.
Both attachGlobalHandlers and gitfaultHandler accept an EnrichConfig:
| Option | Default | Description |
|---|---|---|
cwd |
process.cwd() |
Directory to run git commands in |
contextLines |
5 |
Lines of source shown above/below the throw site |
commitCount |
5 |
Number of recent commits to fetch for the file |
timeoutMs |
2000 |
Kill a git subprocess after this many ms |
ignore |
[] |
Strings (substring match) or RegExps — files matching skip git/source lookups entirely |
Plus, on attachGlobalHandlers only:
| Option | Default | Description |
|---|---|---|
sinks |
[terminalSink] |
Where enriched errors are sent |
exitOnUncaughtException |
true |
Call process.exit(1) after handling an uncaught exception, matching Node's default crash behavior |
exitOnUnhandledRejection |
true |
Same, for unhandled rejections |
Everything above is built on one pure function — no I/O side effects beyond the git/fs calls it explicitly makes, and it never throws:
import { enrich } from 'gitfault';
try {
riskyThing();
} catch (error) {
const enriched = enrich(error);
console.log(enriched.blame, enriched.source, enriched.recentCommits);
}enrich() returns an EnrichedError:
interface EnrichedError {
name: string;
message: string;
stack: string | undefined;
frames: StackFrame[]; // every parsed frame in the stack
throwSite: StackFrame | null; // best guess at the frame that threw
ignored: boolean; // true if throwSite matched an `ignore` pattern
blame: BlameInfo | null;
recentCommits: CommitLogEntry[];
source: SourceSnippet | null;
}The lower-level pieces are exported too, in case you want to compose your
own pipeline: parseStack, getBlame, getRecentCommits,
getSourceSnippet, formatEnrichedError, buildSlackMessage.
- Zero runtime dependencies.
gitis invoked directly vianode:child_process; formatting uses hand-rolled ANSI codes, not chalk/boxen. - Never crashes your app. Every git/fs lookup degrades to
null/[]on failure — not in a git repo, file untracked, git binary missing, subprocess timeout, sink throwing — enrichment is best-effort. - Stateless. No error grouping or deduplication across process restarts; the Slack rate limiter's memory lives only as long as the sink instance.
MIT