Unified Error Handling — a project by Ahsan Mahmood
Zero-dependency TypeScript library with one consistent API to capture, enrich, and route errors to ten monitoring services, loaded on demand so you only…
Changing crash reporters costs a fortnight in a codebase that never planned to.
The SDK is never in one place. Its calls are spread through forty files, its types have leaked into your own function signatures, its breadcrumb API is attached to every interesting event, and by the time you have pulled all of that out you have changed behaviour you never meant to touch.
So most teams never switch.
They pay the renewal instead, which is lock-in doing its work quietly, bought at the moment of the first install rather than at the moment of the invoice.
Unified Error Handling is a thin layer that keeps the decision open.
Underneath it there is very little: a singleton error store, a BaseAdapter contract and adapters loaded by dynamic import. Sentry, Firebase Crashlytics, Bugsnag, Rollbar, Datadog, LogRocket, Raygun, AppCenter, a console adapter for development, and any adapter you write yourself against the same contract.
Changing vendor becomes one line.
The core carries zero runtime dependencies and sits at roughly 7 KB brotli. Because every adapter is a dynamic import, a project reporting to one provider never ships the other nine.
That number is the point of the exercise. An abstraction that costs more than the SDK it wraps is an abstraction nobody keeps past the first performance review.
An optional React entry point adds error boundaries, around twelve hooks and around nine higher-order components, all provider-less, so nothing needs wrapping in context before it can report a failure.
Two behaviours matter more than the length of the provider list.
The first is the offline queue. An error thrown on a train is the error most worth having, and it is exactly the one a direct SDK call drops, so errors are queued and flushed when the connection returns.
The second is enrichment. A stack trace tells you what broke. Breadcrumbs and context tell you what the person was doing at the time, which is the difference between a bug you can reproduce and a ticket open for a month.
There is a discipline this layer does not remove.
An error payload is one of the places personal data escapes a product. Centralising the reporting path makes it possible to scrub in one place instead of ten, and it does not decide what counts as personal for your product. That judgement stays yours.
What it cost was staying thin. Every provider has a feature that maps onto none of the others. Each of those is an argument for widening the contract until it becomes a union of ten SDKs, which is the failure mode of every abstraction shaped like this one, so the contract stayed small and the escape hatch is a custom adapter.
There is a stated limit too. This is a routing layer rather than a monitoring product: it stores nothing, charts nothing, and does not replace the vendor whose dashboard you actually read on a bad morning.
It runs in browsers, in Node.js and in React Native. There is no iOS release behind this work, because there is no Apple Developer account here.
MIT, on npm as unified-error-handling, with the source at github.com/aoneahsan/unified-error-handling.
Sentry is my own default. This package is the reason that choosing it once was never the same as choosing it for good, and reversibility is the only thing it is really selling.
What it does, and what that costs to build
- One API for 10 error-monitoring providers
- Zero runtime dependencies (~7 KB brotli core)
- Dynamic import() adapter loading
- Provider-less React error boundaries + hooks
- Offline queue (errors flush on reconnect)
- Context enrichment + breadcrumbs
- Dual ESM/CJS with conditional exports
Built with
- TypeScript
- esbuild
- React 19
- Vitest
Worth knowing
- npm
- package
- error-handling
- observability
- typescript
- zero-dependency
Who built Unified Error Handling?
Ahsan Mahmood wrote it, including all ten adapters and the optional React entry point. One person could, because the layer is deliberately small: a singleton, an adapter contract and a set of dynamic imports, rather than a reimplementation of anybody else's SDK. It is on npm as unified-error-handling under MIT, with the source at github.com/aoneahsan/unified-error-handling. Sentry is the default in his own projects, and this package is the reason that choice never had to be a permanent one.
Why an abstraction over Sentry instead of using Sentry directly?
Because the second decision is the expensive one. Installing an SDK takes an afternoon; taking one out takes a fortnight, since its calls, its types and its breadcrumb API end up spread through the codebase. This layer keeps the vendor behind one contract, so a change of provider is an adapter swap rather than a migration. If you are certain you will never move, use the SDK directly and skip this. An abstraction nobody ever exercises is only a layer to read through at three in the morning.
Does Unified Error Handling work on iPhone?
It is a JavaScript library, so it runs wherever your JavaScript already runs, which includes a browser on an iPhone and a React Native build. What does not exist here is an iOS release of anything: there is no Apple Developer account behind this work, and nothing I build has shipped to the App Store. The practical answer is that it will report an error from a Safari session on an iPhone exactly as it reports one from Chrome, and none of that involves a platform I have shipped to.
What happens to an error thrown while the device is offline?
It goes into a queue and is sent when the connection comes back. That is the case worth building for, because an error thrown on a train or in a lift is the one a direct SDK call quietly drops, and those are disproportionately the interesting ones. The queue lives in the core rather than in a provider adapter, so it behaves the same whichever vendor is enabled. What it cannot do is survive the user closing the application before reconnecting, and no client-side reporter can.
How much does it add to a bundle?
Roughly 7 KB brotli for the core, with no runtime dependencies underneath it. Every adapter is a dynamic import, so a project reporting to one provider never ships the other nine, and the package is tree-shakeable with dual ESM and CommonJS builds behind conditional exports. That number is not decoration. An abstraction costing more than the SDK it wraps is one a team deletes during the first performance review, which is why staying thin was the constraint the whole design was written under.
https://aoneahsan.com/projects/com.aoneahsan.unifiederrorhandling