Error handling is one of those things we all do — but rarely design properly.
In most projects, error handling grows organically:
- a few try/catch blocks
- some HTTP status checks
- random error messages sent from the backend
- UI logic guessing what went wrong Eventually, the system becomes fragile.
This article walks through how and why I designed a universal error-handling system that works across Backend (Node.js / Express / Next.js APIs) and Frontend (React / Next.js UI) — using a shared contract, not tight coupling.
The Real Problem with Error Handling
Most applications suffer from the same issues:
Backend problems
- Errors from DB, validation, or external APIs look different
- Internal stack traces leak to clients
- Error responses change over time
- Hard to debug production issues
Frontend problems
- Every API returns a different error shape
- UI shows technical or confusing messages
- Error handling logic is duplicated everywhere
- Runtime crashes cause white screens The core issue isn’t missing try/catch.
👉 The issue is lack of design.
The Key Insight: Errors Are a Contract
Instead of treating errors as exceptions, I started treating them as data.
That led to one fundamental idea:
Frontend and backend should share an error contract, not implementations.
This single decision shaped the entire system.
Design Principle #1: Shared Contract, Not Tight Coupling
The frontend and backend do not depend on each other’s code.
They only agree on:
- error codes
- error structure
Example error contract:
{
"success": false,
"message": "User already exists",
"code": "DUPLICATE_ERROR",
"details": {
"field": "email"
},
"traceId": "abc-123"
}
This gives us:
- consistency
- backward compatibility
- freedom to evolve each layer independently
Design Principle #2: Backend Decides What Happened
The backend is the source of truth.
Its responsibility is to:
- detect what went wrong
- categorize the error
- attach structured metadata
- assign a stable error code
It does not decide how the error is shown.
- Internally, the backend normalizes:
- database errors
- validation failures
- authentication & authorization errors
- third-party API failures
- system-level crashes
Everything becomes a single, predictable error object.
Design Principle #3: Frontend Decides How to Show It
The frontend owns user experience.
It receives structured error data and decides:
- the message shown to users
- localization (i18n)
- UI presentation (toast, banner, modal)
- retry or recovery behavior
This allows the same backend error to be shown differently in:
- admin dashboards
- consumer apps
- mobile vs desktop UIs
Technical message ≠ UI message — and that’s intentional.
Design Principle #4: Progressive Adoption
This system is not all-or-nothing.
You can:
- use only backend utilities
- use only frontend hooks
- use both together for best results
Even if the backend doesn’t follow the contract perfectly, the frontend falls back safely.
This makes the library practical for:
- legacy systems
- gradual refactors
- monorepos with mixed stacks
Design Principle #5: Hooks-Only, Modern React
All frontend APIs are built with hooks:
- no class components
- no outdated patterns
Some examples:
-
useAPIError()– handle API & network errors -
useAsyncData()– async operations with built-in error state -
useGlobalError()– app-wide error state -
useNetworkStatus()– offline/online detection
Runtime UI crashes are handled via a React error boundary, wrapped once at the app root.
Design Principle #6: TypeScript-First
TypeScript isn’t an add-on — it’s the foundation.
- strict typing
- shared types between FE & BE
- discriminated unions for error types
- strong inference for consumers
This means:
- fewer runtime surprises
- safer refactors
- better IDE experience
Types are the documentation.
End-to-End Flow (How Everything Connects)
Here’s the full journey of an error:
Backend detects a failure
- Error is normalized into a standard structure
- API returns a predictable error response
- Frontend parses the error
- Error code is mapped to a user-friendly message
- UI displays a safe, meaningful response
At no point does the UI guess what went wrong.
Why This Matters in Production
This approach gives you:
- cleaner APIs
- better UX
- safer production behavior
- easier debugging (via trace IDs)
- a shared mental model for teams
Error handling stops being “glue code” and becomes infrastructure.
The Result: A Universal Error Handler
I packaged this system as an open-source npm library:
npm install @shubhamgupta-oss/universal-error-handler
It works with:
- React
- Next.js
- Node.js
- Express
- TypeScript
- React 18 & 19
GitHub repo: https://github.com/shubhamgupta-oss/universal-error-handler
NPM package: https://www.npmjs.com/package/@shubhamgupta-oss/universal-error-handler
Final Thoughts
Good error handling isn’t about catching errors.
It’s about:
- defining responsibility
- designing contracts
- respecting separation of concerns
Once you treat errors as a first-class system, everything else becomes simpler.
If you’re building full-stack applications and struggling with inconsistent error handling, I hope this approach helps.
Feedback, suggestions, and contributions are welcome. 🙌
Top comments (0)