Skip to content

bombillazo/error-x

Repository files navigation

error-x

npm downloads npm npm

A smart, isomorphic, and satisfying error library for TypeScript applications. Provides type-safe error handling with great DX, solving common pain points like unknown error types, lost stack traces, async error handling, and error serialization.

Features

  • 🎯 Type-safe error handling for great DX
  • 🔄 Smart error conversion from various formats (API responses, strings, Error objects)
  • 📝 Auto-formatted messages and error codes with proper capitalization and punctuation
  • 👤 User-friendly messages separate from technical messages
  • 🕒 Automatic timestamps for error tracking
  • 🔗 Error chaining with cause preservation and stack trace preservation
  • 📊 Flexible metadata for additional context
  • 🎛️ Error handling options for UI behavior and application actions
  • 📦 Serialization/deserialization support for network transfer and storage

Installation

pnpm add @bombillazo/error-x
# or
npm install @bombillazo/error-x
# or
yarn add @bombillazo/error-x

Warning

This library is currently in pre-v1.0 development. While we strive to minimize breaking changes, the API may evolve based on feedback and real-world usage. We recommend pinning to specific versions and reviewing release notes when updating.

Once we reach version 1.0, we plan to minimize API changes and follow semantic versioning.

Quick Start

import { ErrorX, HandlingTargets, type HandlingTarget, type ErrorAction } from 'error-x'

// Minimal usage (all parameters optional)
const error = new ErrorX()

// Simple usage
const error = new ErrorX({ message: 'Database connection failed' })

// With full options
const error = new ErrorX({
  message: 'User authentication failed',
  name: 'AuthError',
  code: 'AUTH_FAILED',
  uiMessage: 'Please check your credentials and try again',
  cause: originalError, // Chain errors while preserving stack traces
  metadata: { 
    userId: 123, 
    loginAttempt: 3,
  },
  actions: [
    { action: 'notify', payload: { targets: [HandlingTargets.TOAST, HandlingTargets.BANNER] } },
    { action: 'redirect', payload: { redirectURL: '/login', delay: 1000 } },
    { action: 'custom', payload: { type: 'analytics', event: 'auth_failed', userId: 123, category: 'errors', severity: 'high' } }
  ]
})

Documentation

API Reference

For complete API documentation with detailed descriptions, examples, and type information, see:

Constructor

new ErrorX(options?: {
  name?: string                      // Optional: Error type
  message?: string                   // Optional: Technical error message (default: 'An error occurred')
  code?: string | number             // Optional: Error code (auto-generated from name if not provided)
  uiMessage?: string                 // Optional: User-friendly message
  cause?: Error | unknown            // Optional: Original error that caused this (preserves stack traces)
  metadata?: Record<string, any>     // Optional: Additional context data
  actions?: ErrorAction[]            // Optional: Configuration for application actions to perform when error occurs 
})

All parameters are optional - ErrorX uses sensible defaults and auto-generates missing values.

Properties

Property Type Default Value Description
name string 'Error' Error type/title
code string Auto-generated from name or 'ERROR' Error identifier (auto-generated from name in UPPER_SNAKE_CASE)
message string 'An error occurred' Auto-formatted technical error message
uiMessage string | undefined undefined User-friendly message for display
stack string Auto-generated Stack trace with preservation and cleaning (inherited from Error)
cause unknown undefined Original error that caused this (preserves full error chain)
timestamp Date new Date() When the error was created (readonly)
metadata Record<string, any> | undefined undefined Additional context and data
actions ErrorAction[] | undefined undefined Array of actions to perform when error occurs (readonly)

Actions System

The actions property allows errors to trigger application logic, passing along the necessary data. Your application error handler can route or execute these actions to achieve the desired behavior.

actions accepts an array of ErrorAction objects. The library provides predefined action types with type-safe payloads, and a CustomAction type for application-specific actions.

Action Types

Action Type Action Value Required Payload Description
NotifyAction 'notify' { targets: HandlingTarget[], ...any } Display notification in specified UI targets
LogoutAction 'logout' { ...any } (optional) Log out the current user
RedirectAction 'redirect' { redirectURL: string, ...any } Redirect to a specific URL
CustomAction 'custom' { ...any } (optional) Application-specific actions with flexible payload structure
import { HandlingTargets, type ErrorAction, type CustomAction } from 'error-x'

// Predefined actions with typed payloads
const error1 = new ErrorX({
  message: 'Payment failed',
  actions: [
    { action: 'notify', payload: { targets: [HandlingTargets.MODAL] } },
    { action: 'redirect', payload: { redirectURL: '/payment', delay: 2000 } }
  ]
})

// Logout action
const error2 = new ErrorX({
  message: 'Session expired',
  actions: [
    { action: 'logout', payload: { clearStorage: true } },
    { action: 'notify', payload: { targets: [HandlingTargets.TOAST] } }
  ]
})

// Custom actions for application-specific logic
const error3 = new ErrorX({
  message: 'API rate limit exceeded',
  actions: [
    { 
      action: 'custom', 
      payload: { 
        type: 'show-rate-limit-modal', 
        resetTime: Date.now() + 60000,
        message: 'Too many requests. Please wait.' 
      } 
    },
    { 
      action: 'custom', 
      payload: { 
        type: 'analytics-track', 
        event: 'rate_limit_hit', 
        severity: 'warning',
        category: 'api'
      } 
    },
    { 
      action: 'custom', 
      payload: { 
        type: 'cache-request', 
        retryAfter: 60,
        endpoint: '/api/users'
      } 
    }
  ]
})

Notify Targets

For the NotifyAction, notify targets can be predefined enum values or custom strings for flexibility:

Predefined Display Targets

Target Enum Value Description
MODAL 'modal' Display in a modal dialog
TOAST 'toast' Display as a toast notification
INLINE 'inline' Display inline with content
BANNER 'banner' Display as a banner/alert bar
CONSOLE 'console' Log to browser/server console
LOGGER 'logger' Send to logging service
NOTIFICATION 'notification' System notification
import { HandlingTargets, type HandlingTarget } from 'error-x'

const error = new ErrorX({
  message: 'Mixed error',
  actions: [
    { 
      action: 'notify', 
      payload: { 
        targets: [
          HandlingTargets.CONSOLE,  // Predefined
          'my-custom-logger',       // Custom
          HandlingTargets.BANNER,   // Predefined
          'analytics-tracker'       // Custom
        ]
      }
    }
  ]
})

Smart Features

Auto Code Generation

Error codes are automatically generated from the error name:

new ErrorX({ message: 'Failed', name: 'DatabaseError' })
// code: 'DATABASE_ERROR'

new ErrorX({ message: 'Failed', name: 'userAuthError' })  
// code: 'USER_AUTH_ERROR'

new ErrorX({ message: 'Failed', name: 'API Timeout' })
// code: 'API_TIMEOUT'

Message Formatting

Messages are automatically formatted with proper capitalization and punctuation:

new ErrorX({ message: 'database connection failed' })
// message: 'Database connection failed.'

new ErrorX({ message: 'user not found. please check credentials' })
// message: 'User not found. Please check credentials.'

Usage Examples

Basic Error Handling

import { ErrorX } from 'error-x'

function validateUser(user: unknown) {
  if (!user) {
    throw new ErrorX({
      message: 'User validation failed: user is required',
      name: 'ValidationError',
      code: 'USER_REQUIRED',
      uiMessage: 'Please provide user information',
      metadata: { field: 'user', received: user }
    })
  }
}

API Error Handling

async function fetchUser(id: string) {
  try {
    const response = await fetch(`/api/users/${id}`)
    if (!response.ok) {
      throw new ErrorX({
        message: `Failed to fetch user: ${response.statusText}`,
        code: `HTTP_${response.status}`,
        uiMessage: 'Unable to load user data',
        metadata: { status: response.status, statusText: response.statusText }
      })
    }
    return response.json()
  } catch (error) {
    // Convert any error to ErrorX and add context
    const errorX = ErrorX.toErrorX(error)
    throw errorX.withMetadata({
      userId: id,
      operation: 'fetchUser',
    })
  }
}

Error Chaining and Stack Trace Preservation

try {
  await database.transaction(async (tx) => {
    await tx.users.create(userData)
  })
} catch (dbError) {
  // Create new ErrorX while preserving the original error in the cause chain
  const error = new ErrorX({
    message: 'User creation failed',
    name: 'UserCreationError',
    code: 'USER_CREATE_FAILED',
    uiMessage: 'Unable to create user account',
    cause: dbError, // Preserves original stack trace and error details
    metadata: {
      operation: 'userRegistration',
      userData: { email: userData.email } // Don't log sensitive data
    }
  })
  
  // Add more context while preserving the error chain
  throw error.withMetadata({
    requestId: generateRequestId(),
    userAgent: request.headers['user-agent']
  })
}

FAQ

Why use action type "custom" instead of an open string type for CustomAction?

The ErrorAction type uses a discriminated union based on the action property. When you use arbitrary values instead of the predefined action types ('notify', 'logout', 'redirect', 'custom'), it breaks TypeScript's ability to properly narrow the payload types.

The Problem: If ErrorAction allowed any string as the action type, TypeScript would default to the most permissive payload type ({ ...any }) for all actions, causing type definition to leak between different action types.

// ❌ Cannot be done - breaks discriminated union
const error = new ErrorX({
  actions: [
    { action: 'analytics', payload: { event: 'error' } }, // Loses type safety
    { action: 'notify', payload: { targets: ['toast'] } }, // Payload type becomes too permissive
    { action: 'redirect', payload: { redirectURL: '/home' } } // Required properties not enforced
  ]
})

// ✅ Do this - maintains proper type discrimination
const error = new ErrorX({
  actions: [
    { action: 'custom', payload: { type: 'analytics', event: 'error' } },
    { action: 'notify', payload: { targets: ['toast'] } }, // Properly typed with required 'targets'
    { action: 'redirect', payload: { redirectURL: '/home' } } // Properly typed with required 'redirectURL'
  ]
})

The Solution: Using action: 'custom' with a discriminating type property in the payload preserves the discriminated union while allowing unlimited flexibility for custom actions. This approach:

  • Maintains type safety for predefined actions (notify, logout, redirect)
  • Provides a structured way to handle custom application logic
  • Allows your error handlers to properly switch on action types
  • Enables you to create your own discriminated unions within custom payloads

Ideally, we would support custom action types directly in the action. If there is a solution to this problem, we are more than happy to review it. Please open an issue or PR!.

License

MIT