A smart, isomorphic, and type-safe error library for TypeScript applications. Provides excellent DX with intelligent error conversion, stack trace preservation, serialization support, and HTTP error presets.
- 🎯 Type-safe error handling with full TypeScript support
- 🔄 Smart error conversion from various formats (API responses, strings, Error objects)
- 👤 User-friendly messages separate from technical messages
- 🔗 Error chaining with cause preservation and stack trace preservation
- 📊 Flexible metadata for additional context
- 📦 Serialization/deserialization for network transfer and storage
- 🎨 HTTP error presets for all status codes (400-511)
- 🌐 Isomorphic - works in Node.js and browsers
- ⚙️ Global configuration for defaults and documentation URLs
pnpm add @bombillazo/error-x
# or
npm install @bombillazo/error-x
# or
yarn add @bombillazo/error-x- Node.js: 18 or higher
- TypeScript: 5.0 or higher (optional, but recommended)
- Target Environment: ES2022+
This library uses modern JavaScript features and ES2022 APIs. For browser compatibility, ensure your build tool (e.g., Vite, webpack, esbuild) is configured to target ES2022 or transpile accordingly.
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.
import { ErrorX, http } from '@bombillazo/error-x'
// Simple usage
throw new ErrorX('Database connection failed')
// With options
throw new ErrorX({
message: 'User authentication failed',
name: 'AuthError',
code: 'AUTH_FAILED',
uiMessage: 'Please check your credentials and try again',
metadata: { userId: 123, loginAttempt: 3 },
source: 'auth-service'
})
// Using HTTP presets
throw new ErrorX(http[404])
// Customizing presets
throw new ErrorX({
...http[401],
message: 'Session expired',
metadata: { userId: 123 }
})
// Smart conversion from unknown errors
try {
await someOperation()
} catch (error) {
const errorX = ErrorX.from(error)
throw errorX.withMetadata({ context: 'additional info' })
}new ErrorX(input?: string | ErrorXOptions)All parameters are optional. ErrorX uses sensible defaults:
| Property | Type | Default Value | Description |
|---|---|---|---|
| message | string |
'An error occurred' |
Technical error message (pass-through, no auto-formatting) |
| name | string |
'Error' |
Error type/title |
| code | string | number |
Auto-generated from name or 'ERROR' |
Error identifier (auto-generated from name in UPPER_SNAKE_CASE) |
| uiMessage | string | undefined |
undefined |
User-friendly message for display |
| cause | ErrorXCause | Error | unknown |
undefined |
Original error that caused this (preserves full error chain) |
| metadata | Record<string, unknown> | undefined |
undefined |
Additional context and data (flexible storage for any extra info) |
| type | string | undefined |
undefined |
Error type for categorization (e.g., 'http', 'validation') |
| docsUrl | string | undefined |
undefined or auto-generated |
Documentation URL for this specific error |
| source | string | undefined |
undefined or from config |
Where the error originated (service name, module, component) |
| timestamp | number |
Date.now() |
Unix epoch timestamp in milliseconds when error was created |
| stack | string |
Auto-generated | Stack trace with preservation and cleaning (inherited from Error) |
ErrorX provides pre-configured error templates via the http export:
import { ErrorX, http } from '@bombillazo/error-x'
// Use preset directly
throw new ErrorX(http[404])
// Result: 404 error with message "Not found.", code "NOT_FOUND", etc.
// Override specific fields
throw new ErrorX({
...http[404],
message: 'User not found',
metadata: { userId: 123 }
})
// Add error cause
try {
// some operation
} catch (originalError) {
throw new ErrorX({
...http[500],
cause: originalError,
metadata: { operation: 'database-query' }
})
}All presets are indexed by HTTP status code (numeric keys) and include:
code: Error code in UPPER_SNAKE_CASEname: Descriptive error namemessage: Technical message with proper sentence casing and perioduiMessage: User-friendly messagemetadata: Contains{ status: <number> }with the HTTP status code
HTTP presets work well because HTTP status codes are universally standardized. For domain-specific errors (database, validation, authentication, business logic), create your own presets:
import { type ErrorXOptions } from '@bombillazo/error-x'
// Define your application-specific presets
export const dbErrors = {
connectionFailed: {
name: 'DatabaseError',
code: 'DB_CONNECTION_FAILED',
message: 'Database connection failed.',
uiMessage: 'Unable to connect to database. Please try again later.',
type: 'database',
},
queryTimeout: {
name: 'DatabaseError',
code: 'DB_QUERY_TIMEOUT',
message: 'Database query timeout.',
uiMessage: 'The operation took too long. Please try again.',
type: 'database',
},
} satisfies Record<string, ErrorXOptions>;
export const authErrors = {
invalidToken: {
name: 'AuthenticationError',
code: 'AUTH_INVALID_TOKEN',
message: 'Invalid authentication token.',
uiMessage: 'Your session has expired. Please log in again.',
metadata: { status: 401 },
type: 'authentication',
},
insufficientPermissions: {
name: 'AuthorizationError',
code: 'AUTH_INSUFFICIENT_PERMISSIONS',
message: 'Insufficient permissions.',
uiMessage: 'You do not have permission to perform this action.',
metadata: { status: 403 },
type: 'authorization',
},
} satisfies Record<string, ErrorXOptions>;
// Use them just like http presets
throw new ErrorX(dbErrors.connectionFailed);
throw new ErrorX({ ...authErrors.invalidToken, metadata: { userId: 123 } });This approach keeps your error handling consistent while remaining flexible for your specific domain.
import { ErrorX } from '@bombillazo/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 }
})
}
}async function fetchUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new ErrorX({
...http[response.status === 404 ? 404 : 500],
metadata: { status: response.status, statusText: response.statusText }
})
}
return response.json()
} catch (error) {
const errorX = ErrorX.from(error)
throw errorX.withMetadata({ userId: id, operation: 'fetchUser' })
}
}try {
await database.transaction(async (tx) => {
await tx.users.create(userData)
})
} catch (dbError) {
throw new ErrorX({
message: 'User creation failed',
name: 'UserCreationError',
code: 'USER_CREATE_FAILED',
uiMessage: 'Unable to create user account',
cause: dbError, // Preserves original stack trace
metadata: {
operation: 'userRegistration',
email: userData.email
}
})
}Important: ErrorX does NOT auto-format messages. Messages are passed through as-is:
new ErrorX({ message: 'test error' })
// message: 'test error' (exactly as provided)
new ErrorX({ message: 'Test error.' })
// message: 'Test error.' (exactly as provided)Empty or whitespace-only messages default to 'An error occurred':
new ErrorX({ message: '' })
// message: 'An error occurred'
new ErrorX({ message: ' ' })
// message: 'An error occurred'HTTP presets provide properly formatted messages with sentence casing and periods.
Error codes are automatically generated from names when not provided:
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'MIT