unified-error-handling
v2.1.1
Published
One error-capture API for your app, with the tracking SDK loaded only when you use it.
Maintainers
Readme
Docs · npm · GitHub · Changelog · AI Guide · Support
[!IMPORTANT] Errors only reach a service once you activate an adapter.
initialize()on its own captures nothing — callawait useAdapter('<name>', config)too, or every captured error is discarded. See Quick Start.
unified-error-handling gives your application one small API — captureError, setUser,
addBreadcrumb — and forwards it to whichever error-tracking service you point it at. The vendor SDK is
loaded through a dynamic import() at the moment you activate its adapter, so a project that only uses
Sentry never pays for the other eight. The library itself has no runtime dependencies, and the React layer
needs no context provider, so hooks work in any component.
| | |
|---|---|
| Version | 2.1.1 |
| License | MIT |
| Node | >=24.13.0 |
| Platforms | Browser · Node.js |
| Install size | ~7 kB min+brotli (core) · ~10 kB (React layer) |
| Types | Bundled .d.ts (ESM + CJS) |
| Status | Stable · actively maintained |
🧭 Table of Contents #
- 💡 Why unified-error-handling
- ✨ Features
- 📱 Platform Support
- 📋 Requirements
- 📦 Installation
- 🚀 Quick Start
- 🛠️ Usage
- ⚙️ Configuration
- 🔧 API Reference
- 🧩 Types
- 🧪 Examples
- 🎛️ Advanced Features
- 🚑 Recovery & Troubleshooting
- 🚧 Limitations
- ❓ FAQ
- 📚 Documentation
- 🔄 Changelog
- 🤝 Contributing
- 💬 Support
- 📄 License
- 👤 Author
- 🔗 Links
- 🏷️ Keywords
💡 Why unified-error-handling #
Error-tracking vendors each ship their own SDK, their own initialisation ritual, and their own idea of what a "user" or a "breadcrumb" is. Wiring one in spreads vendor-specific calls through your codebase, and swapping vendors later means touching every one of those call sites.
This library puts one thin layer in front of them. Your code calls captureError; the adapter translates.
Changing vendor is a one-line change at startup.
| | unified-error-handling | Importing a vendor SDK directly |
|---|---|---|
| Call sites | vendor-neutral | vendor-specific throughout |
| Switching vendor | one line at startup | edit every call site |
| Runtime dependencies | none | the vendor SDK, always |
| SDK cost when unused | none — loaded on activation | bundled whether used or not |
| Vendor-specific features | only what the adapter maps | everything the vendor offers |
Not the right tool when you need a vendor's advanced features — Sentry performance tracing, LogRocket session search, Bugsnag release pipelines. This layer maps errors, messages, user context and breadcrumbs; anything past that is easier reached by using the vendor SDK directly. It is also the wrong choice if you want errors delivered to several services at once — see Limitations.
✨ Features #
- Nine adapters — console, Sentry, Firebase, DataDog, Bugsnag, Rollbar, LogRocket, Raygun and AppCenter.
- Dynamic SDK loading — a vendor SDK is imported only when its adapter is activated.
- Zero runtime dependencies — nothing is added to your dependency tree.
- No React provider — hooks read a module singleton, so they work in any component at any depth.
- Automatic capture — unhandled errors, promise rejections,
console.errorand failed network requests. - Context and breadcrumbs — user, tags, device and a capped breadcrumb trail travel with every error.
- Offline queueing — errors captured while the browser is offline are held and flushed on reconnect.
beforeSendhook — rewrite an error or drop it by returningnull.- Custom adapters — send errors to your own endpoint with one
sendfunction. - Typed throughout — bundled declarations for both ESM and CJS.
📱 Platform Support #
| Platform | Supported | Notes |
|---|---|---|
| Browser | ✅ | Full support, including global handlers, offline queue and interceptors. |
| Node.js | ⚠️ | Manual captureError works. Global handlers are not installed — they attach to window, so process exceptions are not captured. |
| React Native | ⚠️ | Not verified by the maintainer. The core has no browser-only imports, but the offline queue and global handlers depend on window. |
📋 Requirements #
| Requirement | Version | Why |
|---|---|---|
| Node | >=24.13.0 | the version the package is built and tested on |
| react | >=19.0.0 | optional peer — only for unified-error-handling/react |
| react-dom | >=19.0.0 | optional peer — only for unified-error-handling/react |
Vendor SDKs are not dependencies. Install only the one your adapter needs — see Usage for the per-adapter list.
📦 Installation #
yarn add unified-error-handlingNo build step, no native sync, no configuration file. Add the vendor SDK for whichever adapter you plan to activate, for example:
yarn add @sentry/browser🚀 Quick Start #
Two calls at startup: initialise the store, then activate an adapter.
import { initialize, useAdapter, captureError } from 'unified-error-handling';
initialize({ enableGlobalHandlers: true });
await useAdapter('console');
captureError(new Error('Something went wrong'));Swap 'console' for a real service when you are ready — nothing else in your code changes:
await useAdapter('sentry', { dsn: process.env.SENTRY_DSN });🛠️ Usage #
Choosing an adapter
Each adapter loads its SDK on activation. Install the SDK yourself; the library never bundles it.
| Adapter | SDK to install | Required config |
|---|---|---|
| console | none — built in | none |
| sentry | @sentry/browser | dsn |
| datadog | @datadog/browser-rum + @datadog/browser-logs | applicationId, clientToken |
| bugsnag | @bugsnag/js | apiKey |
| rollbar | rollbar | accessToken |
| logrocket | logrocket | appId |
| raygun | raygun4js | apiKey |
| appcenter | appcenter-crashes + appcenter-analytics | appSecret |
| firebase | firebase | firebaseConfig — ⚠️ see Limitations |
Per-adapter options are documented at Adapters.
Adding context
import { setUser, addBreadcrumb, captureError } from 'unified-error-handling';
setUser({ id: '12345', email: '[email protected]' });
addBreadcrumb({ message: 'Opened checkout', category: 'ui', level: 'info' });
captureError(new Error('Payment declined'), {
tags: { feature: 'checkout' },
extra: { orderId: 'A-4471' },
});React
Initialise once at your entry point, then use the hooks anywhere — no provider to mount.
import { initialize, useAdapter } from 'unified-error-handling';
import { ErrorBoundary, useErrorHandler } from 'unified-error-handling/react';
initialize({ enableGlobalHandlers: true });
await useAdapter('sentry', { dsn: import.meta.env.VITE_SENTRY_DSN });
function App() {
return (
<ErrorBoundary>
<Checkout />
</ErrorBoundary>
);
}
function Checkout() {
const handleError = useErrorHandler();
return <button onClick={() => handleError(new Error('Boom'))}>Pay</button>;
}useErrorHandler() returns a function. For the store's other actions use useErrorStore(), which
returns { captureError, setUser, addBreadcrumb, ... }.
A custom adapter
import { createAdapter, useAdapter } from 'unified-error-handling';
createAdapter('my-backend', {
async send(error, context) {
await fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({ error, context }),
});
},
});
await useAdapter('my-backend');⚙️ Configuration #
Every field of initialize(config) is optional.
| Option | Type | Default | What it does |
|---|---|---|---|
| maxBreadcrumbs | number | 100 | Trail length; oldest are dropped first. |
| enableGlobalHandlers | boolean | true | Capture window errors and unhandled rejections. Browser only. |
| enableOfflineQueue | boolean | true | Hold errors while offline, flush on reconnect. |
| enableConsoleCapture | boolean | true | Capture console.error calls. |
| enableNetworkCapture | boolean | false | Capture failed fetch and XHR requests. |
| beforeSend | (error) => NormalizedError \| null | — | Rewrite an error, or return null to drop it. |
| environment | string | — | Passed through to adapters that accept it. |
| release | string | — | Passed through to adapters that accept it. |
| debug | boolean | false | Log to the console when an error is captured with no active adapter. |
Full reference: Configuration.
🔧 API Reference #
A signature index. Full documentation on the docs site.
Core — unified-error-handling
| Export | Signature | Docs |
|---|---|---|
| initialize | (config?: ErrorStoreConfig) => void | → |
| captureError | (error: Error \| string, context?: Partial<ErrorContext>) => void | → |
| captureMessage | (message: string, level?: string) => void | → |
| setUser | (user: UserContext \| null) => void | → |
| setContext | (context: Partial<ErrorContext>) => void | → |
| addBreadcrumb | (crumb: Omit<Breadcrumb, 'timestamp'>) => void | → |
| clearBreadcrumbs | () => void | → |
| useAdapter | (name: string, config?: unknown) => Promise<void> | → |
| removeAdapter | (name: string) => void | → |
| registerAdapter | (name: string, adapter: ErrorAdapter) => void | → |
| createAdapter | (name: string, config: CustomAdapterConfig) => void | → |
| createCustomAdapter | (config: CustomAdapterConfig) => ErrorAdapter | → |
| subscribe | (listener: ErrorListener) => () => void | → |
| flush | () => Promise<void> | → |
| reset | () => void | → |
| errorStore | the singleton behind every function above | → |
Adapter classes SentryAdapter, FirebaseAdapter and CustomAdapter are also exported for advanced use.
React — unified-error-handling/react
| Export | Signature | Docs |
|---|---|---|
| ErrorBoundary | React.Component<ErrorBoundaryProps> | → |
| withErrorBoundary | (Component, options?) => Component | → |
| useErrorHandler | () => (error, context?) => void | → |
| useErrorStore | () => ErrorStoreActions & { initialized, offline, activeAdapter } | → |
| useAsyncError | () => (error) => void | → |
| useAsyncOperation | <T>(op: () => Promise<T>, deps?) => { data, loading, error, execute } | → |
| useErrorTracking | (componentName: string) => void — logs mount/unmount breadcrumbs | → |
| useComponentError | (componentName: string) => { logComponentError } | → |
| usePerformanceMonitor | () => { measurePerformance } | → |
| useExtendedErrorHandler | () => { logError, logNavigation, logUserAction, setTags } | → |
Higher-order components — withErrorHandler, withAsyncErrorHandler, withPageErrorHandling,
withApiErrorHandling, withFormErrorHandling, withCriticalErrorHandling — are re-exported from the same
entry point.
🧩 Types #
The types a consumer actually touches:
export interface ErrorStoreConfig {
maxBreadcrumbs?: number;
enableGlobalHandlers?: boolean;
enableOfflineQueue?: boolean;
enableConsoleCapture?: boolean;
enableNetworkCapture?: boolean;
beforeSend?: (error: NormalizedError) => NormalizedError | null;
environment?: string;
release?: string;
debug?: boolean;
}
export interface ErrorContext {
user?: UserContext;
device?: DeviceContext;
custom?: Record<string, any>;
tags?: Record<string, string>;
extra?: Record<string, any>;
}
export interface Breadcrumb {
timestamp: number;
message: string;
category?: string;
level?: 'debug' | 'info' | 'warning' | 'error';
data?: Record<string, any>;
}
export interface CustomAdapterConfig {
send: (error: NormalizedError, context: ErrorContext) => Promise<void>;
initialize?: () => Promise<void>;
setContext?: (context: ErrorContext) => Promise<void>;
addBreadcrumb?: (breadcrumb: Breadcrumb) => Promise<void>;
flush?: () => Promise<void>;
close?: () => Promise<void>;
}NormalizedError, UserContext, DeviceContext, ErrorAdapter, ErrorLevel and ErrorListener are
exported alongside them. Full listing:
Types.
🧪 Examples #
| Goal | Example | |---|---| | Capture in plain JavaScript | basic-usage.js | | Wire up React | react-usage.jsx | | Send to Sentry | sentry-adapter.js | | Send to Firebase | firebase-adapter.js | | Write your own adapter | custom-adapter.js |
🎛️ Advanced Features #
- Offline queue — while
navigator.onLineis false, errors are held in memory and flushed when the browser reconnects. → - Interceptors —
console.errorand failedfetch/XHRcalls become captured errors. → - Enrichment — each error gains device, viewport, language and a grouping fingerprint before dispatch.
subscribe(listener)— observe every captured error in-process, for a debug overlay or your own sink.- Custom adapters — one
sendfunction is the whole contract. →
🚑 Recovery & Troubleshooting #
| Symptom | Cause | Fix |
|---|---|---|
| Nothing reaches the service | No adapter is active — the error was discarded | await useAdapter('<name>', config) after initialize() |
| [ErrorStore] Not initialized warning | captureError ran before initialize() | Call initialize() at your entry point |
| Failed to load <sdk> on activation | The vendor SDK is not installed | yarn add <sdk> — see the table in Usage |
| [ErrorStore] Already initialized warning | initialize() called twice | Call it once; it is a module singleton |
| Errors stop after switching adapter | useAdapter replaces the active adapter | Expected — only one is active at a time |
| Nothing captured in Node | Global handlers bind to window | Call captureError yourself, or add your own process hooks |
🚧 Limitations #
Stated as plainly as the features:
- One adapter is active at a time.
useAdapter()makes that adapter the only destination; calling it again replaces the previous one. Errors are not fanned out to several services at once. - With no active adapter, captured errors are dropped. They are logged to the console only when
debug: true. - The
firebaseadapter cannot load in a browser. It importsfirebase/crashlytics, which the Firebase JS SDK does not provide — Crashlytics is a native-only product. Activation fails with a "Failed to load" error. Tracked as ISSUE-002. - Global handlers are browser-only. Node.js gets no
uncaughtExceptionorunhandledRejectionhook. - The offline queue is in-memory. A page reload while offline loses whatever was queued.
reset()cannot remove global handlers. They are registered as anonymous listeners, so a captured error can still reach the store afterreset()within the same page session.- Only the
Sentry,FirebaseandCustomadapter classes are re-exported from the package root. The other six are reachable throughuseAdapter('<name>'), which is the supported path. - The React entry needs React 19+. There is no React 18 build.
engines.nodeis>=24.13.0. Older Node versions produce anEBADENGINEwarning on install.
❓ FAQ #
Can I send errors to two services at once?
Not today. The store keeps a single active adapter. To mirror errors, write a custom adapter whose send
forwards to both destinations, or call subscribe() and dispatch yourself.
Do I have to install all nine vendor SDKs? No. Install only the SDK for the adapter you activate. Nothing else is imported.
Does it work without React?
Yes. The core entry has no React import. The unified-error-handling/react subpath is opt-in, and both
React peers are marked optional.
Why is there no provider component? The store is a module-level singleton, so hooks read it directly. That is why a hook works in any component without wrapping your tree.
How do I stop an error from being sent?
Return null from beforeSend — the error is discarded before it reaches the adapter.
📚 Documentation #
Which document answers which question:
| Document | Read it when | |---|---| | Introduction | deciding whether this fits your project | | Installation | adding it to an app | | Quick start | first time using it | | Configuration | tuning what is captured | | Adapters | wiring a specific vendor | | Custom adapters | sending errors to your own endpoint | | React guide | using the hooks and the boundary | | API reference | you need an exact signature | | Provider overview | comparing the supported services | | AI integration guide | a coding agent is implementing against it |
🔄 Changelog #
Latest release: 2.1.1 — six new adapters (DataDog, Bugsnag, Rollbar, LogRocket, Raygun, AppCenter), a
fix that restores the bundled TypeScript declarations, and no source maps in the published tarball.
Full history: CHANGELOG.md.
🤝 Contributing #
Fork and open a pull request — see
CONTRIBUTING.md for setup,
standards, and how to request collaborator access. main is protected: every change lands through a
reviewed PR.
💬 Support #
Questions and bugs: open an issue.
If this package saves you time, you can support its maintenance at aoneahsan.com/payment.
📄 License #
MIT © Ahsan Mahmood — see LICENSE.
👤 Author #
Ahsan Mahmood — aoneahsan.com · GitHub · LinkedIn · [email protected]
🔗 Links #
| | | |---|---| | Documentation | https://unified-error-handling-docs.aoneahsan.com | | npm | https://www.npmjs.com/package/unified-error-handling | | Repository | https://github.com/aoneahsan/unified-error-handling | | Issues | https://github.com/aoneahsan/unified-error-handling/issues | | Changelog | https://github.com/aoneahsan/unified-error-handling/blob/main/CHANGELOG.md | | Support the project | https://aoneahsan.com/payment |
🏷️ Keywords #
error-handling · crash-reporting · error-tracking · sentry · bugsnag · rollbar · datadog · logrocket · raygun · zero-dependency · react · typescript
