DEV Community

Cover image for Designing a Universal Error Handler for Frontend & Backend (React + Node.js)
Shubham Gupta
Shubham Gupta

Posted on

Designing a Universal Error Handler for Frontend & Backend (React + Node.js)

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"
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Error is normalized into a standard structure
  2. API returns a predictable error response
  3. Frontend parses the error
  4. Error code is mapped to a user-friendly message
  5. 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

Enter fullscreen mode Exit fullscreen mode

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)