npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@shubhamgupta-oss/universal-error-handler

v1.1.1

Published

Production-grade, universal error-handling system for Node.js/Express + React. Single shared error contract for full-stack TypeScript applications.

Downloads

143

Readme

Production-grade, universal error-handling system for Node.js/Express + React

A single, standardized npm package that provides comprehensive error handling across your full stack. Built with TypeScript, industry-proven patterns, and real-world error scenarios.

What This Solves:

  • ❌ No more scattered error handling logic between frontend and backend
  • ❌ No more translating technical errors into user-friendly messages manually
  • ❌ No more inconsistent error response formats
  • ✅ One standardized error format & contract for your entire application

🎯 Key Features

  • Single Shared Core - One error contract used by both backend & frontend
  • Automatic Error Normalization - Converts any error type to standard format
  • Backend Adapters - Express middleware, asyncHandler, DB mappers, validation factories
  • Frontend Hooks - React hooks for error management, recovery, offline detection, and data fetching
  • Type-Safe - 100% TypeScript with strict mode for all utilities
  • Production-Ready - Process-level handlers, graceful shutdown, request correlation
  • Human-Friendly Messages - Automatic translation of technical errors into user-friendly copy
  • Comprehensive Coverage - 37 error codes for different scenarios (validation, DB, auth, payments, etc.)
  • Zero Configuration - Works out-of-the-box with sensible defaults
  • Customizable - Override messages, add custom error codes, extend behavior

⚙️ How It Works (The Flow)

The Problem It Solves

WITHOUT universal-error-handler:
┌────────────────┐         ┌──────────────────┐
│ Backend Error  │────────→│ Raw JSON Response│
└────────────────┘         └──────────────────┘
                                    ↓
                           "Invalid syntax"  (Too technical!)
                           "Cast to ObjectId failed"
                           → All inconsistent formats & messaging


WITH universal-error-handler:
┌──────────────────────────┐
│ Backend Error (any type) │
└────────────┬─────────────┘
             ↓
    ┌────────────────────┐
    │ ErrorNormalizer    │ ← Converts to UniversalError
    └────────────────────┘
             ↓
    ┌──────────────────────────────────┐
    │ UniversalError                   │ ← Standard format
    │ {code, message, details, traceId}│   - code: "DUPLICATE_KEY"
    │                                  │   - message: "Email exists"
    └────────────┬─────────────────────┘   - traceId: "uuid"
                 ↓
    ┌────────────────────┐
    │ HTTP Response      │
    └────────────┬───────┘
                 ↓
    ┌────────────────────────────┐
    │ Frontend APIErrorParser    │ ← Receives structured error
    └────────────┬───────────────┘
                 ↓
    ┌────────────────────────────┐
    │ UIMessageMapper            │ ← Maps to friendly message:
    │ "DUPLICATE_KEY" →          │   "This email is already registered"
    └────────────┬───────────────┘
                 ↓
            Show to user! ✅

Step-by-Step Example

1. Backend throws standardized error:

// In your Express route handler
app.post('/users', asyncHandler(async (req, res) => {
  const existing = await User.findByEmail(req.body.email);
  if (existing) {
    throw new UniversalError(
      ErrorCode.DUPLICATE_KEY,
      "User with this email already exists"
    );
  }
  const user = await User.create(req.body);
  res.json({ success: true, data: user });
}));

2. Error middleware catches and sends standardized response:

{
  "success": false,
  "code": "DUPLICATE_KEY",
  "message": "User with this email already exists",
  "traceId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2024-01-21T10:30:00.000Z"
}

3. Frontend receives API response:

const res = await fetch('/api/users', {
  method: 'POST',
  body: JSON.stringify({ email: '[email protected]' })
});
const data = await res.json();
// data = { success: false, code: "DUPLICATE_KEY", ... }

4. Frontend parses and converts to UI message:

const { error, handleError } = useAPIError();
handleError(data);

// error is now:
{
  code: "DUPLICATE_KEY",
  technicalMessage: "User with this email already exists",
  uiMessage: "This email is already registered.",
  traceId: "550e8400-e29b-41d4-a716-446655440000"
}

5. Display to user:

{error && (
  <div className="error-message">
    {error.uiMessage} {/* Shows: "This email is already registered." ✅ */}
  </div>
)}

📦 Architecture

universal-error-handler/
├── src/
│   ├── shared/           # ← Used by Backend AND Frontend
│   │   ├── error-codes.ts       # Single source of truth for error codes
│   │   ├── types.ts             # Shared interfaces
│   │   ├── error.ts             # UniversalError class
│   │   └── error-normalizer.ts  # Converts any error to UniversalError
│   │
│   ├── backend/          # ← Node.js / Express only
│   │   ├── async-handler.ts           # Eliminates try-catch boilerplate
│   │   ├── error-middleware.ts        # Express error middleware
│   │   ├── db-error-mapper.ts         # MongoDB + SQL error mapping
│   │   ├── process-handler.ts         # Unhandled rejections, exceptions
│   │   ├── request-id-middleware.ts   # Request correlation
│   │   ├── validation-error-factory.ts # Zod, Joi, custom validators
│   │   └── external-api-error-handler.ts # Payment, email, 3rd-party APIs
│   │
│   └── frontend/         # ← React only
│       ├── ui-message-mapper.ts      # Error code → human-friendly message
│       ├── api-error-parser.ts       # Parse API responses
│       ├── use-api-error.ts          # React hook for API errors
│       ├── use-global-error.tsx      # Global error context
│       ├── error-boundary.tsx        # Error boundary component
│       ├── use-network-status.ts     # Online/offline detection
│       └── use-async-data.ts         # Data fetching with error handling
│
└── examples/
    ├── backend/
    │   ├── express-api.ts      # Complete Express example
    │   └── error-patterns.ts   # How to throw errors
    └── frontend/
        ├── components.tsx      # React components with error handling
        └── error-config.ts     # Error message customization

📋 What's Included & How to Use Each Part

Shared Core (Used by Both Frontend & Backend)

| Component | Purpose | How to Use | |-----------|---------|-----------| | ErrorCode (enum) | 37 predefined error codes for different scenarios | throw new UniversalError(ErrorCode.DUPLICATE_KEY, message) | | UniversalError (class) | Standardized error class with code, message, status, trace ID | Use instead of native Error class | | ErrorNormalizer | Converts ANY error type (native JS, Zod, Joi, DB errors) to UniversalError | Auto-used in backend middleware; can be used manually | | error-codes.ts | Maps error codes to HTTP status codes (400, 401, 403, 404, 500, etc.) | Reference for understanding which status each error returns |

Backend Utilities (Node.js/Express)

| Utility | What It Does | How to Use | When to Use | |---------|--------------|-----------|-----------| | requestIdMiddleware | Adds unique trace ID to every request for correlation | app.use(requestIdMiddleware) (first middleware) | Every Express app (helps with debugging) | | asyncHandler | Wraps route handlers to automatically catch errors | app.post('/route', asyncHandler(async (req, res) => {...})) | All route handlers (eliminates try-catch) | | errorMiddleware | Express middleware that catches errors and sends standardized response | app.use(errorMiddleware()) (last middleware) | All Express apps (required for error handling) | | ValidationErrorFactory | Creates validation errors from Zod, Joi, or custom validators | throw ValidationErrorFactory.fromZodError(zodError) | Form validation, input validation | | DatabaseErrorMapper | Converts MongoDB/SQL errors to UniversalError | throw DatabaseErrorMapper.map(mongoError) | Database operations that might fail | | ExternalAPIErrorHandler | Handles errors from payment, email, and 3rd-party APIs | throw ExternalAPIErrorHandler.handlePaymentError(stripeError) | Payment processing, email sending, external API calls | | ProcessErrorHandler | Global error handler for uncaught exceptions and rejections | ProcessErrorHandler.initialize({...}) (once at startup) | Production safety net |

Frontend Utilities (React)

| Hook/Component | What It Does | How to Use | When to Use | |----------------|--------------|-----------|-----------| | useAPIError | Manages error state for single API calls | const {error, handleError} = useAPIError() | When handling individual API requests | | useAsyncData | Manages data fetching with built-in error handling | const {data, error, isLoading} = useAsyncData(() => fetch(...), {autoExecute: true}) | Loading data from APIs with auto-fetch | | useGlobalError | App-wide error state via Context API | <GlobalErrorProvider><MyApp/></GlobalErrorProvider> | Sharing errors across entire app | | ErrorBoundary | React component that catches rendering errors | <ErrorBoundary><App/></ErrorBoundary> | Catching React component crashes | | useNetworkStatus | Detects online/offline status | const {isOnline} = useNetworkStatus() | Showing offline banners | | APIErrorParser | Parses API responses and converts to structured errors | const error = APIErrorParser.parseError(response) | Parsing API error responses | | UIMessageMapper | Maps error codes to human-friendly UI messages | UIMessageMapper.getMessage(ErrorCode.DUPLICATE_KEY) | Converting codes to user messages |

🎯 Complete Step-by-Step Setup Guide

Backend Setup (5 steps)

Step 1: Install and import

import express from 'express';
import {
  requestIdMiddleware,
  ProcessErrorHandler,
  asyncHandler,
  errorMiddleware,
} from 'universal-error-handler/backend';
import { UniversalError, ErrorCode } from 'universal-error-handler/shared';

Step 2: Add request ID middleware (FIRST)

const app = express();
app.use(express.json());
app.use(requestIdMiddleware); // ← Must be first to track all requests

Step 3: Initialize process-level error handling (ONCE)

ProcessErrorHandler.initialize({
  isDevelopment: process.env.NODE_ENV !== 'production',
  logger: (error) => {
    // Optional: Send to monitoring service like Sentry, DataDog
    console.error(`[${error.code}] ${error.message}`);
  },
  gracefulShutdown: async () => {
    // Optional: Close DB connections, cleanup
    console.log('Shutting down gracefully...');
  },
});

Step 4: Use asyncHandler for all routes

app.post('/api/users', asyncHandler(async (req, res) => {
  // NO try-catch needed! asyncHandler catches everything
  
  if (!req.body.email) {
    throw new UniversalError(
      ErrorCode.MISSING_REQUIRED_FIELD,
      'Email is required'
    );
  }

  const user = await User.create(req.body);
  res.status(201).json({ success: true, data: user });
}));

app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new UniversalError(ErrorCode.NOT_FOUND, 'User not found');
  }
  res.json({ success: true, data: user });
}));

Step 5: Add error middleware (LAST)

// Must be AFTER all routes
app.use(errorMiddleware({
  isDevelopment: process.env.NODE_ENV !== 'production',
}));

app.listen(3000, () => console.log('Server running on 3000'));

Frontend Setup (4 steps)

Step 1: Install and import

import React from 'react';
import {
  ErrorBoundary,
  GlobalErrorProvider,
  useAPIError,
} from 'universal-error-handler/frontend';

Step 2: Wrap app with error providers (REQUIRED)

// In your root App component or main.tsx/index.tsx
function App() {
  return (
    <ErrorBoundary
      level="global"
      onError={(error) => {
        console.error('Caught error:', error.uiMessage);
      }}
    >
      <GlobalErrorProvider>
        <YourAppContent />
      </GlobalErrorProvider>
    </ErrorBoundary>
  );
}

Step 3: Optional - Customize error messages

import { UIMessageMapper } from 'universal-error-handler/frontend';
import { ErrorCode } from 'universal-error-handler/shared';

// Do this once at app startup
UIMessageMapper.registerMessages({
  [ErrorCode.DUPLICATE_KEY]: 'This already exists in the system.',
  [ErrorCode.UNAUTHORIZED]: 'Please sign in to continue.',
  [ErrorCode.PAYMENT_ERROR]: 'Payment failed. Try another card.',
});

Step 4: Use hooks in components

function LoginForm() {
  const { error, handleError, clearError, isLoading } = useAPIError();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    clearError(); // Clear previous errors

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      const data = await response.json();

      if (!data.success) {
        handleError(data); // ← Converts to UI-friendly error
        return;
      }

      // Success - redirect
      navigate('/dashboard');
    } catch (err) {
      handleError(err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />

      {error && (
        <div style={{ color: 'red', marginTop: '10px' }}>
          {error.uiMessage}
        </div>
      )}

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Detailed Usage

Backend: Validation Errors

import { ValidationErrorFactory } from 'universal-error-handler/backend';

// Zod
app.post('/users', asyncHandler(async (req, res) => {
  const result = createUserSchema.safeParse(req.body);
  if (!result.success) {
    throw ValidationErrorFactory.fromZodError(result.error);
  }
  // ...
}));

// Joi
try {
  await joiSchema.validateAsync(data);
} catch (error) {
  throw ValidationErrorFactory.fromJoiError(error);
}

// Custom fields
throw ValidationErrorFactory.fromFieldErrors({
  email: 'Invalid email format',
  age: 'Must be at least 18',
});

// Missing field
throw ValidationErrorFactory.missingField('password');

// Invalid input
throw ValidationErrorFactory.invalidInput('Age must be positive');

Backend: Database Errors

import { DatabaseErrorMapper } from 'universal-error-handler/backend';

// MongoDB
try {
  await User.create(userData);
} catch (error) {
  throw DatabaseErrorMapper.mapMongoError(error);
}

// SQL (Prisma, TypeORM, etc.)
try {
  await prisma.user.create({ data });
} catch (error) {
  throw DatabaseErrorMapper.mapSqlError(error);
}

// Auto-detect
try {
  // ... db operation
} catch (error) {
  throw DatabaseErrorMapper.map(error);
}

Backend: External API Errors

import { ExternalAPIErrorHandler } from 'universal-error-handler/backend';

// Payment errors
try {
  await stripe.charges.create({ ... });
} catch (error) {
  throw ExternalAPIErrorHandler.handlePaymentError(error);
}

// Email errors
try {
  await sendgrid.send({ ... });
} catch (error) {
  throw ExternalAPIErrorHandler.handleEmailError(error);
}

// Generic HTTP errors
try {
  await fetch('https://api.service.com/data').then(r => r.json());
} catch (error) {
  throw ExternalAPIErrorHandler.handleHttpError(error);
}

Backend: Process-Level Handlers

import { ProcessErrorHandler } from 'universal-error-handler/backend';

// Initialize ONCE in your app
ProcessErrorHandler.initialize({
  isDevelopment: process.env.NODE_ENV !== 'production',
  logger: (error) => {
    // Send to monitoring service (Sentry, DataDog, etc.)
    monitoringService.captureException(error);
  },
  gracefulShutdown: async () => {
    // Close DB connections, cleanup, etc.
    await db.disconnect();
    await redis.quit();
  },
});

// Automatically handles:
// - Uncaught exceptions
// - Unhandled promise rejections
// - Graceful shutdown

Frontend: React Hooks

// useAPIError - Single API call error handling
function LoginForm() {
  const { error, handleError, clearError } = useAPIError();

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const res = await fetch('/api/login', { ... });
      const data = await res.json();
      if (!data.success) {
        handleError(data);
        return;
      }
    } catch (err) {
      handleError(err);
    }
  };

  return (
    <>
      {error && <ErrorToast message={error.uiMessage} />}
      <form onSubmit={handleSubmit}>...</form>
    </>
  );
}

// useAsyncData - Fetch with automatic error handling
function UserList() {
  const {
    data,
    error,
    isLoading,
    refetch,
    reset,
  } = useAsyncData(
    () => fetch('/api/users').then(r => r.json()),
    { autoExecute: true }
  );

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} onRetry={refetch} />;
  return <div>{data.map(...)}</div>;
}

// useNetworkStatus - Offline detection
function App() {
  const { isOnline, offlineMessage } = useNetworkStatus();

  return (
    <>
      {!isOnline && <OfflineBanner message={offlineMessage} />}
      <MainContent />
    </>
  );
}

// useGlobalError - App-wide error state
function RootApp() {
  return (
    <GlobalErrorProvider>
      <ErrorToastContainer /> {/* Reads from useGlobalError */}
      <MyApp />
    </GlobalErrorProvider>
  );
}

// useErrorHandler - Catch runtime errors
function SafeComponent() {
  const { error } = useErrorHandler((error) => {
    // Send to monitoring service
    sentry.captureException(error);
  });

  if (error) return <Fallback />;
  return <MyContent />;
}

Frontend: Error Boundary

// Catches React rendering errors
<ErrorBoundary
  level="global"
  fallback={({ uiMessage, traceId }) => (
    <div>
      <h1>{uiMessage}</h1>
      <p>Error ID: {traceId}</p>
      <button onClick={() => window.location.reload()}>Reload</button>
    </div>
  )}
  onError={(error) => {
    // Send to monitoring service
    sentry.captureException(error);
  }}
>
  <App />
</ErrorBoundary>

� Error Codes Reference

The package includes 37 error codes covering common scenarios:

Generic Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | INTERNAL_SERVER_ERROR | 500 | Unexpected server error | | UNKNOWN_ERROR | 500 | Error type can't be determined |

Validation Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | VALIDATION_ERROR | 400 | Input validation fails (from Zod, Joi, etc.) | | INVALID_INPUT | 400 | User input is malformed or invalid | | MISSING_REQUIRED_FIELD | 400 | Required field is missing from request |

Authentication & Authorization

| Code | HTTP Status | Use When | |------|-------------|----------| | UNAUTHORIZED | 401 | User not authenticated | | AUTH_FAILED | 401 | Authentication attempt failed | | INVALID_CREDENTIALS | 401 | Wrong username/password | | INVALID_TOKEN | 401 | JWT or auth token is invalid | | TOKEN_EXPIRED | 401 | Token has expired | | FORBIDDEN | 403 | User authenticated but not authorized | | INSUFFICIENT_PERMISSIONS | 403 | User lacks required permissions |

Resource Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | NOT_FOUND | 404 | Resource doesn't exist | | RESOURCE_NOT_FOUND | 404 | Specific resource not found |

Database Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | DATABASE_ERROR | 500 | Generic database error | | DUPLICATE_KEY | 409 | Unique constraint violation (duplicate email, etc.) | | CAST_ERROR | 400 | Invalid type for database field | | SCHEMA_VALIDATION_ERROR | 400 | Data doesn't match database schema | | CONNECTION_ERROR | 500 | Can't connect to database |

External API Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | EXTERNAL_API_ERROR | 502 | Generic 3rd-party API error | | PAYMENT_ERROR | 502 | Payment processing failed | | EMAIL_ERROR | 500 | Email sending failed | | THIRD_PARTY_ERROR | 502 | Other 3rd-party service error |

Network Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | NETWORK_ERROR | 503 | Network is down or unreachable | | TIMEOUT_ERROR | 504 | Request timeout | | DNS_ERROR | 503 | DNS resolution failed | | OFFLINE | 503 | Frontend is offline |

File Operation Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | FILE_UPLOAD_ERROR | 400 | File upload failed | | FILE_SIZE_EXCEEDED | 413 | File is too large | | INVALID_FILE_TYPE | 400 | File type not allowed |

Frontend-Specific Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | CHUNK_LOAD_ERROR | 500 | Failed to load JavaScript chunk | | BUILD_ERROR | 500 | Frontend build failed | | RUNTIME_ERROR | 500 | Runtime JavaScript error | | UI_CRASH | 500 | React component crashed |

Business Logic Errors

| Code | HTTP Status | Use When | |------|-------------|----------| | BUSINESS_LOGIC_ERROR | 400 | Business rule violated | | OPERATION_FAILED | 400 | Operation couldn't complete | | CONFLICT | 409 | State conflict (e.g., order already shipped) |

🔧 Advanced Usage

Adding Custom Error Codes

Step 1: Extend the enum in src/shared/error-codes.ts:

export enum ErrorCode {
  // ... existing codes
  CUSTOM_PAYMENT_ISSUE = 'CUSTOM_PAYMENT_ISSUE',
  INVENTORY_LOW = 'INVENTORY_LOW',
  SUBSCRIPTION_EXPIRED = 'SUBSCRIPTION_EXPIRED',
}

Step 2: Map to HTTP status in HTTP_STATUS_CODES:

export const HTTP_STATUS_CODES: Record<ErrorCode, number> = {
  // ... existing mappings
  [ErrorCode.CUSTOM_PAYMENT_ISSUE]: 402, // 402 = Payment Required
  [ErrorCode.INVENTORY_LOW]: 409, // 409 = Conflict
  [ErrorCode.SUBSCRIPTION_EXPIRED]: 403, // 403 = Forbidden
};

Step 3: Use in backend:

throw new UniversalError(
  ErrorCode.INVENTORY_LOW,
  'Only 5 items left in stock',
  { details: { required: 10, available: 5 } }
);

Step 4: Customize frontend message (optional):

UIMessageMapper.registerMessages({
  [ErrorCode.CUSTOM_PAYMENT_ISSUE]: 'Payment was declined. Try another card.',
  [ErrorCode.INVENTORY_LOW]: 'Not enough items in stock.',
  [ErrorCode.SUBSCRIPTION_EXPIRED]: 'Your subscription has expired.',
});

Integration with Monitoring Services (Sentry, DataDog, etc.)

Send all errors to Sentry:

import * as Sentry from "@sentry/node";

// Backend
ProcessErrorHandler.initialize({
  logger: (error) => {
    // Send to Sentry with context
    Sentry.captureException(error, {
      tags: {
        errorCode: error.code,
      },
    });
  },
});

Frontend error tracking:

import * as Sentry from "@sentry/react";

<ErrorBoundary
  onError={(error) => {
    Sentry.captureException(error, {
      tags: { errorCode: error.code },
    });
  }}
>
  <GlobalErrorProvider>
    <App />
  </GlobalErrorProvider>
</ErrorBoundary>

Manual Error Context Tracking

Use traceId to correlate errors across frontend and backend:

Backend:

const traceId = (req as any).id; // From requestIdMiddleware
const error = new UniversalError(ErrorCode.PAYMENT_ERROR, "Charge failed", {
  traceId, // Include for correlation
  details: { amount: 99.99, provider: 'stripe' }
});

Frontend:

if (error) {
  // error.traceId is available
  console.error(`Error (${error.traceId}): ${error.uiMessage}`);
  
  // Send to support or logging service with traceId
  await sendToSupport({
    message: error.uiMessage,
    traceId: error.traceId,
    timestamp: error.timestamp,
  });
}

�🔐 Production Best Practices

Backend

  1. Never expose stack traces to clients:

    // ✅ Good
    throw new UniversalError(
      ErrorCode.DATABASE_ERROR,
      'Database operation failed'
    );
    
    // ❌ Bad
    throw new UniversalError(
      ErrorCode.DATABASE_ERROR,
      error.stack // Exposes internal details!
    );
  2. Use request IDs for correlation:

    const traceId = (req as any).id; // From requestIdMiddleware
    // Include in logs, monitoring, etc.
  3. Log sensitive info server-side only:

    errorMiddleware({
      logger: (error, req) => {
        // Log full error details only on backend
        if (process.env.NODE_ENV === 'development') {
          console.error(error); // Include stack trace
        } else {
          console.error(error.code, error.message); // Safe logging
        }
      },
    });
  4. Handle process-level errors:

    ProcessErrorHandler.initialize({
      gracefulShutdown: async () => {
        // Cleanup and exit cleanly
      },
    });

Frontend

  1. Never show raw API responses:

    // ✅ Good
    const uiError = APIErrorParser.parseError(apiResponse);
    showToast(uiError.uiMessage);
    
    // ❌ Bad
    showToast(apiResponse.message); // May be too technical
  2. Use error boundaries:

    <ErrorBoundary>
      <App /> {/* Catches all rendering errors */}
    </ErrorBoundary>
  3. Monitor critical errors:

    <GlobalErrorProvider>
      <ErrorTracker /> {/* Send errors to Sentry, etc. */}
      <App />
    </GlobalErrorProvider>
  4. Handle offline gracefully:

    const { isOnline } = useNetworkStatus();
    if (!isOnline) {
      return <OfflineScreen />;
    }

📊 Error Coverage Checklist

Backend Errors Handled ✓

  • [x] Sync errors (SyntaxError, ReferenceError, etc.)
  • [x] Async/Promise errors
  • [x] Express route errors
  • [x] Validation (Zod, Joi, custom)
  • [x] Authentication (401)
  • [x] Authorization (403)
  • [x] Not found (404)
  • [x] Database (MongoDB duplicate, cast, schema)
  • [x] Database (SQL errors, Prisma, TypeORM)
  • [x] Payment API errors
  • [x] Email service errors
  • [x] 3rd-party API errors
  • [x] Timeout errors
  • [x] Network/DNS errors
  • [x] File upload errors
  • [x] Business logic errors
  • [x] Unhandled promise rejections
  • [x] Uncaught exceptions (process-level)

Frontend Errors Handled ✓

  • [x] API response errors
  • [x] Network offline
  • [x] Request timeout
  • [x] Runtime JS errors (useErrorHandler)
  • [x] React rendering errors (ErrorBoundary)
  • [x] Promise rejections
  • [x] Form validation errors
  • [x] Global app crashes (ErrorBoundary)
  • [x] Graceful fallback UI
  • [x] Error code → UI message mapping

🎨 Example: Full-Stack Feature

Scenario: User Registration

Backend:

app.post(
  '/auth/register',
  asyncHandler(async (req, res) => {
    // Validation
    const result = registerSchema.safeParse(req.body);
    if (!result.success) {
      throw ValidationErrorFactory.fromZodError(result.error);
    }

    // Check duplicate
    try {
      const existing = await User.findByEmail(result.data.email);
      if (existing) {
        throw new UniversalError(
          ErrorCode.DUPLICATE_KEY,
          'Email already registered'
        );
      }

      // Create user
      const user = await User.create(result.data);

      // Send email (with error handling)
      try {
        await emailService.sendWelcome(user);
      } catch (error) {
        throw ExternalAPIErrorHandler.handleEmailError(error);
      }

      res.status(201).json({ success: true, data: { id: user.id } });
    } catch (error) {
      throw DatabaseErrorMapper.map(error);
    }
  })
);

Frontend:

function RegisterForm() {
  const {
    error,
    isLoading,
    handleError,
    clearError,
    isValidationError,
  } = useAPIError();
  const [formData, setFormData] = useState({ email: '', password: '' });

  const handleSubmit = async (e) => {
    e.preventDefault();
    clearError();

    try {
      const res = await fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });

      const data = await res.json();

      if (!data.success) {
        handleError(data);
        return;
      }

      // Success - redirect
      navigate('/login');
    } catch (error) {
      handleError(error);
    }
  };

  const validationIssues = error
    ? APIErrorParser.getValidationIssues(error)
    : [];

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email"
      />

      {error && isValidationError && (
        <ul style={{ color: 'red' }}>
          {validationIssues.map((issue) => (
            <li key={issue.path}>{issue.message}</li>
          ))}
        </ul>
      )}

      {error && !isValidationError && (
        <div style={{ color: 'red' }}>{error.uiMessage}</div>
      )}

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Registering...' : 'Register'}
      </button>
    </form>
  );
}

Result:

  • User enters invalid email → Validation error with field-level messages
  • User enters duplicate email → "This email is already registered."
  • Email service fails → "Email service unavailable. Try again later."
  • Network timeout → "Request timed out. Please check your connection."
  • Success → Redirected to login

🧪 Testing

// Backend error testing
import { UniversalError, ErrorCode } from 'universal-error-handler/shared';

it('should throw duplicate key error', () => {
  expect(() => {
    throw new UniversalError(
      ErrorCode.DUPLICATE_KEY,
      'Email exists'
    );
  }).toThrow(UniversalError);
});

// Frontend error parsing testing
import { APIErrorParser } from 'universal-error-handler/frontend';

it('should parse API error correctly', () => {
  const response = {
    success: false,
    code: 'DUPLICATE_KEY',
    message: 'Email exists',
  };

  const uiError = APIErrorParser.parseError(response);
  expect(uiError.uiMessage).toBe('This email is already registered.');
});

❓ Common Questions & Troubleshooting

Backend Questions

Q: Do I need to wrap ALL routes with asyncHandler?

A: Yes. asyncHandler catches errors thrown in route handlers. Without it, errors won't be caught properly. It's a small wrapper that replaces try-catch boilerplate.

Q: What if I forget to add errorMiddleware as the last middleware?

A: Errors won't be caught and sent as standardized responses. The middleware must be LAST in the middleware stack.

Q: Can I use custom error codes?

A: Yes! Extend the ErrorCode enum in src/shared/error-codes.ts. The frontend will automatically handle new codes (falling back to a default message until you customize it).

Q: What's the difference between ValidationErrorFactory.missingField() and ValidationErrorFactory.invalidInput()?

A: missingField() is for required fields that are missing. invalidInput() is for fields that are present but invalid. Both send a 400 status.

Q: How do I log errors without exposing stack traces to clients?

A: Use the logger option in ProcessErrorHandler.initialize() for process-level errors (and errorMiddleware() logger for request errors):

ProcessErrorHandler.initialize({
  logger: (error) => {
    if (process.env.NODE_ENV === 'production') {
      console.error(`[${error.code}] ${error.message}`); // Safe
    } else {
      console.error(error); // Include stack trace in dev
    }
  }
});

Q: What does ProcessErrorHandler actually do?

A: It catches unhandled promise rejections and uncaught exceptions at the process level. Without it, your app crashes silently. It's a safety net.

Frontend Questions

Q: What's the difference between useAPIError, useAsyncData, and useGlobalError?

A:

  • useAPIError: For single API calls in one component
  • useAsyncData: For fetching data with automatic retry/loading
  • useGlobalError: For app-wide errors (multiple components)

Q: Do I need both ErrorBoundary and GlobalErrorProvider?

A: Yes. ErrorBoundary catches React rendering crashes. GlobalErrorProvider manages API/async errors. They work together.

Q: How do I show different messages for the same error code?

A: Use UIMessageMapper.registerMessages() to customize:

UIMessageMapper.registerMessages({
  [ErrorCode.DUPLICATE_KEY]: 'This email is already in use.',
  [ErrorCode.NETWORK_ERROR]: 'Check your internet connection.',
});

Q: How do I detect validation errors specifically?

A: Use error.details?.issues or APIErrorParser.isValidationError(error):

if (APIErrorParser.isValidationError(error)) {
  error.details?.issues?.forEach(issue => {
    setFieldError(issue.path, issue.message);
  });
}

Q: How do I handle offline mode?

A: Use useNetworkStatus():

const { isOnline, offlineMessage } = useNetworkStatus();
if (!isOnline) return <OfflinePage />;

Integration Questions

Q: Can I use this with Next.js?

A: Yes! Use for API routes (backend) and React components (frontend). The package works independently of the framework.

Q: Can I use this with GraphQL?

A: Yes! Backend still uses asyncHandler. Frontend use the same hooks. Just adapt error parsing to your GraphQL response format.

Q: What if my API doesn't return the standard format?

A: You can parse it manually and convert to the expected format before passing to hooks:

const customError = {
  code: ErrorCode.CUSTOM_ERROR,
  message: response.error_message,
  traceId: response.request_id,
};
handleError(customError);

⚠️ Common Mistakes to Avoid

Backend Mistakes

Mistake 1: Forgetting asyncHandler wrapper

// ❌ Wrong - Error not caught
app.post('/users', async (req, res) => {
  throw new UniversalError(...);
});

// ✅ Right
app.post('/users', asyncHandler(async (req, res) => {
  throw new UniversalError(...);
}));

Mistake 2: Putting errorMiddleware before routes

// ❌ Wrong - Won't catch route errors
app.use(errorMiddleware());
app.post('/users', asyncHandler(...));

// ✅ Right
app.post('/users', asyncHandler(...));
app.use(errorMiddleware());

Mistake 3: Exposing internal errors to clients

// ❌ Wrong - Exposes database details
throw new UniversalError(
  ErrorCode.DATABASE_ERROR,
  `MongoDB error: ${error.message}` // Exposed!
);

// ✅ Right
throw new UniversalError(
  ErrorCode.DATABASE_ERROR,
  'Database operation failed' // Safe
);

Mistake 4: Not initializing ProcessErrorHandler

// ❌ Wrong - No safety net for uncaught errors
app.listen(3000);

// ✅ Right
ProcessErrorHandler.initialize({ isDevelopment: true });
app.listen(3000);

Frontend Mistakes

Mistake 1: Forgetting ErrorBoundary

// ❌ Wrong - React crash crashes entire app
export default function App() {
  return <MyApp />;
}

// ✅ Right
export default function App() {
  return (
    <ErrorBoundary>
      <GlobalErrorProvider>
        <MyApp />
      </GlobalErrorProvider>
    </ErrorBoundary>
  );
}

Mistake 2: Not checking response.success

// ❌ Wrong - Treats error response as success
const res = await fetch('/api/data');
const data = await res.json();
// data might have success: false, but you treat it as success

// ✅ Right
const res = await fetch('/api/data');
const data = await res.json();
if (!data.success) {
  handleError(data); // Parse as error
  return;
}
// Use data safely

Mistake 3: Showing raw message instead of uiMessage

// ❌ Wrong - Too technical for users
{error && <div>{error.message}</div>}
// Shows: "Cast to ObjectId failed"

// ✅ Right
{error && <div>{error.uiMessage}</div>}
// Shows: "Item not found"

Mistake 4: Not catching both API and network errors

// ❌ Wrong - Doesn't handle network failure
const data = await fetch('/api/data').then(r => r.json());
handleError(data); // Crashes if network fails

// ✅ Right
try {
  const res = await fetch('/api/data');
  const data = await res.json();
  if (!data.success) {
    handleError(data);
    return;
  }
} catch (err) {
  handleError(err); // Handle network errors too
}

📄 License

MIT


Happy error handling! 🎯