react-rescuer
v0.1.4
Published
React error boundaries with automatic recovery, observability, breadcrumbs, and DevOverlay
Maintainers
Readme
react-rescuer
Error boundaries for React 18 — with automatic recovery, observability, and zero config DevOverlay.
Why react-rescuer?
React's built-in error boundaries catch render errors — and that's it. react-rescuer adds automatic retry with backoff, structured observability (breadcrumbs, fingerprinting, session tracking), a zero-config DevOverlay in development, and testing utilities. All opt-in, zero config to start.
Install
npm i react-rescuer
# pnpm add react-rescuer | yarn add react-rescuer | bun add react-rescuerPeer deps: react >= 18, react-dom >= 18
Quick start
import { ErrorBoundary } from "react-rescuer";
export function App() {
return (
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Page />
</ErrorBoundary>
);
}That's it for the happy path. Everything below is opt-in.
Features
Fallback UI
Three modes — pick the one that fits:
// static node
<ErrorBoundary fallback={<p>Error</p>}>
// render prop — access error + reset
<ErrorBoundary fallbackRender={({ error, resetError }) => (
<div>
<p>{error.message}</p>
<button onClick={resetError}>Retry</button>
</div>
)}>
// component
<ErrorBoundary FallbackComponent={MyFallback}>FallbackProps received by fallbackRender / FallbackComponent:
type FallbackProps = {
error: Error;
errorContext: ErrorContext; // fingerprint, breadcrumbs, sessionId, …
resetError: () => void;
retryCount: number;
};Automatic recovery
Pass a recovery prop to retry automatically with exponential backoff:
import { ErrorBoundary } from "react-rescuer";
<ErrorBoundary
recovery={{
maxRetries: 3,
retryDelay: (attempt) => Math.min(8000, 250 * 2 ** (attempt - 1)), // 250 → 500 → 1000 …
isRecoverable: (error) => error.name !== "FatalError",
onMaxRetriesReached: (error, ctx) => reportToSentry(error, ctx),
}}
fallbackRender={({ error, retryCount }) => (
<p>
{error.message} — attempt {retryCount}
</p>
)}
>
<DataWidget />
</ErrorBoundary>;For orchestrating retries across multiple boundaries, use RetryManager from react-rescuer/recovery:
import { RetryManager, createExponentialBackoff } from "react-rescuer/recovery";
const manager = new RetryManager(
{ maxRetries: 5 },
createExponentialBackoff(250, 10_000),
);
const { ok, delayMs } = manager.next("widget-boundary", error, context);Observability
Every error gives you a structured ErrorContext out of the box:
type ErrorContext = {
error: Error;
fingerprint: string; // stable hash across deploys
breadcrumbs: Breadcrumb[]; // last 20 user actions before the crash
componentStack: string;
sessionId: string;
errorCount: number;
timestamp: number;
};Auto-capture clicks and navigation events, then attach them to the boundary:
import { ErrorBoundary } from "react-rescuer";
import { addBreadcrumb, buildErrorContext } from "react-rescuer/observability";
// manually add a breadcrumb anywhere in your app
addBreadcrumb({ type: "custom", message: "user submitted form" });
<ErrorBoundary
contextBuilder={buildErrorContext}
onError={(error, _info, ctx) => {
sendToMonitoring({ error, ctx }); // fingerprint + breadcrumbs included
}}
fallback={<p>Something went wrong.</p>}
>
<CheckoutForm />
</ErrorBoundary>;getBreadcrumbTrail() auto-starts on first call and captures click, pushState, replaceState, and popstate. Breadcrumbs clear on boundary reset.
DevOverlay
In development, ErrorBoundary automatically renders a built-in overlay with the error, stack, component tree, retries left, and breadcrumbs. No setup needed — it tree-shakes to zero in production.
Async errors
React's componentDidCatch only catches render-time errors. Use useErrorBoundary to route async or event-handler errors into the nearest boundary:
import { ErrorBoundary } from "react-rescuer";
import { useErrorBoundary } from "react-rescuer/hooks";
function SaveButton() {
const { showBoundary } = useErrorBoundary();
return (
<button
onClick={async () => {
try {
await api.save();
} catch (e) {
showBoundary(e as Error);
}
}}
>
Save
</button>
);
}
<ErrorBoundary fallback={<p>Save failed.</p>}>
<SaveButton />
</ErrorBoundary>;HOC
import { withErrorBoundary } from "react-rescuer/hoc";
const SafeWidget = withErrorBoundary(Widget, {
fallback: <p>Widget failed to load.</p>,
});Testing
import { render } from "@testing-library/react";
import { createTestBoundary, installMatchers } from "react-rescuer/testing";
installMatchers(); // adds toHaveCaughtError + toHaveCaughtErrorMatching to expect
const tb = createTestBoundary();
const { Boundary, getLastContext } = tb;
function Bomb() {
throw new Error("boom");
return null;
}
render(
<Boundary>
<Bomb />
</Boundary>,
);
expect(tb).toHaveCaughtError();
expect(tb).toHaveCaughtErrorMatching("boom");
expect(getLastContext()?.fingerprint).toBeTruthy();Import paths
import { ErrorBoundary } from "react-rescuer";
import { withErrorBoundary } from "react-rescuer/hoc";
import { useErrorBoundary, useErrorContext } from "react-rescuer/hooks";
import { createTestBoundary, installMatchers } from "react-rescuer/testing";
import { RetryManager, createExponentialBackoff } from "react-rescuer/recovery";
import { addBreadcrumb, buildErrorContext, fingerprintError, getBreadcrumbTrail } from "react-rescuer/observability";Contributing
Issues and PRs welcome. See the repository for setup instructions.
License
MIT © Rody Huancas
