Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,18 @@ jobs:
run: pnpm run test:coverage
if: matrix.node-version == 20

- name: Upload coverage reports
uses: codecov/codecov-action@v4
- name: Verify coverage thresholds
run: |
echo "Coverage thresholds are enforced in vitest.config.ts:"
echo " - Statements: 90%"
echo " - Branches: 85%"
echo " - Functions: 90%"
echo " - Lines: 90%"
echo "Test will fail if thresholds are not met."
if: matrix.node-version == 20

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
if: matrix.node-version == 20
with:
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ node_modules
.ralph-tui
AGENTS.md
dist

# Coverage
coverage/
*.lcov
67 changes: 60 additions & 7 deletions LLMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Quick Start

```typescript
import { ErrorX, HTTPErrorX, DBErrorX, ValidationErrorX } from '@bombillazo/error-x';
import { ErrorX, AggregateErrorX, HTTPErrorX, DBErrorX, ValidationErrorX } from '@bombillazo/error-x';

// Basic usage
throw new ErrorX({ message: 'Operation failed', code: 'OP_FAILED' });
Expand All @@ -22,6 +22,9 @@ throw ValidationErrorX.fromZodError(zodError);
// Error chaining (preserves cause chain)
throw new ErrorX({ message: 'High-level error', cause: originalError });

// Error aggregation (batch operations)
const aggregate = ErrorX.aggregate([error1, error2, error3]);

// Type-safe metadata
const error = new ErrorX<{ userId: number }>({
message: 'User error',
Expand Down Expand Up @@ -62,6 +65,7 @@ new ErrorX<TMetadata>({ metadata: {...} }) // Type-safe metadata
|--------|-------------|
| `ErrorX.from(value, overrides?)` | Wraps any value into ErrorX. Stores original in `.original` property. |
| `ErrorX.fromJSON(serialized)` | Reconstructs ErrorX from serialized form |
| `ErrorX.aggregate(errors, opts?)` | Combines multiple errors into AggregateErrorX |
| `ErrorX.isErrorX(value)` | Type guard: `value is ErrorX` |
| `ErrorX.isErrorXOptions(value)` | Validates if object is valid ErrorXOptions |
| `ErrorX.configure(config)` | Set global config (cleanStack, cleanStackDelimiter) |
Expand Down Expand Up @@ -143,6 +147,50 @@ apiError.chain.length; // 3
new ErrorX({ cause: new Error('native') }); // Works, wraps into ErrorX
```

## Error Aggregation

Combine multiple errors into a single `AggregateErrorX` for batch operations:

```typescript
import { ErrorX, AggregateErrorX } from '@bombillazo/error-x';

// Aggregate validation errors
const errors = [
new ErrorX({ message: 'Email required', code: 'EMAIL_REQUIRED' }),
new ErrorX({ message: 'Password too short', code: 'PASSWORD_SHORT' }),
];
const aggregate = ErrorX.aggregate(errors);
// message: 'Multiple errors occurred (2 errors)', code: 'AGGREGATE_ERROR'

// With custom options
const batchError = ErrorX.aggregate(errors, {
message: 'Validation failed',
code: 'VALIDATION_BATCH',
httpStatus: 400,
metadata: { formId: 'signup' },
});

// Access individual errors
aggregate.errors.forEach(e => console.log(e.code)); // Each preserves its chain

// Type guard
if (AggregateErrorX.isAggregateErrorX(error)) {
console.log(`${error.errors.length} errors`);
}

// Serialization
const json = aggregate.toJSON(); // Includes all aggregated errors
const restored = AggregateErrorX.fromJSON(json);
```

### AggregateErrorX

| Property/Method | Description |
|-----------------|-------------|
| `errors` | `readonly ErrorX[]` - All aggregated errors |
| `AggregateErrorX.isAggregateErrorX(value)` | Type guard |
| `AggregateErrorX.fromJSON(serialized)` | Deserialize aggregate |

## Serialization

```typescript
Expand Down Expand Up @@ -221,12 +269,14 @@ PaymentErrorX.create('DECLINED', { metadata: { transactionId: 'tx_123' } });
```typescript
// Core types
import type {
ErrorXOptions, // Constructor options
ErrorXMetadata, // Record<string, unknown>
ErrorXSerialized, // Serialized form
ErrorXSnapshot, // Original error snapshot
ErrorXConfig, // Global configuration
ErrorXOptionField, // Valid option field names
ErrorXOptions, // Constructor options
ErrorXMetadata, // Record<string, unknown>
ErrorXSerialized, // Serialized form
ErrorXAggregateSerialized, // Serialized aggregate form
ErrorXAggregateOptions, // Aggregate constructor options
ErrorXSnapshot, // Original error snapshot
ErrorXConfig, // Global configuration
ErrorXOptionField, // Valid option field names
} from '@bombillazo/error-x';

// Transform types
Expand Down Expand Up @@ -313,6 +363,9 @@ try {
if (ErrorX.isErrorX(err)) {
console.log(err.code, err.metadata);
}
if (AggregateErrorX.isAggregateErrorX(err)) {
err.errors.forEach(e => console.log(e.message));
}
if (err instanceof HTTPErrorX) {
console.log(err.httpStatus);
}
Expand Down
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![npm downloads](https://img.shields.io/npm/dm/@bombillazo/error-x.svg?style=for-the-badge)](https://www.npmjs.com/package/@bombillazo/error-x)
[![npm](https://img.shields.io/npm/dt/@bombillazo/error-x.svg?style=for-the-badge)](https://www.npmjs.com/package/@bombillazo/error-x)
[![npm](https://img.shields.io/npm/l/@bombillazo/error-x?style=for-the-badge)](https://github.com/bombillazo/error-x/blob/master/LICENSE)
[![codecov](https://img.shields.io/codecov/c/github/bombillazo/error-x?style=for-the-badge)](https://codecov.io/gh/bombillazo/error-x)

🚨❌

Expand All @@ -14,6 +15,7 @@ A smart, isomorphic, and type-safe error library for TypeScript applications. Pr
- **Isomorphic** - works in Node.js and browsers
- **Smart error conversion** from different sources (API responses, strings, Error objects)
- **Error chaining** for full error sequence
- **Error aggregation** for batch operations with multiple failures
- **Factory method** `.create()` for preset-based error creation
- **Custom metadata** with type-safe generics for additional context
- **Global configuration** for stack cleaning and defaults
Expand Down Expand Up @@ -108,6 +110,7 @@ The base error class that extends the native `Error` with enhanced capabilities.
| `from(value, opts?)` | Convert any value to ErrorX with intelligent property extraction |
| `fromJSON(json)` | Deserialize JSON back to ErrorX instance |
| `create(key?, opts?)` | Factory method for preset-based error creation (used by subclasses) |
| `aggregate(errors, opts?)` | Combine multiple errors into an AggregateErrorX instance |
| `isErrorX(value)` | Type guard to check if value is an ErrorX instance |
| `isErrorXOptions(v)` | Check if value is a valid ErrorXOptions object |
| `configure(config)` | Set global configuration (stack cleaning, defaults) |
Expand Down Expand Up @@ -355,6 +358,65 @@ if (ErrorX.isErrorX(error)) {
}
```

### Error Aggregation

Combine multiple errors into a single `AggregateErrorX` instance. Useful for batch operations, parallel processing, or validation scenarios where multiple failures can occur.

```typescript
import { ErrorX, AggregateErrorX } from "@bombillazo/error-x";

// Aggregate multiple validation errors
const validationErrors = [
new ErrorX({ message: "Email is required", code: "EMAIL_REQUIRED" }),
new ErrorX({ message: "Password too short", code: "PASSWORD_SHORT" }),
new ErrorX({ message: "Invalid phone format", code: "PHONE_INVALID" }),
];

const aggregate = ErrorX.aggregate(validationErrors);
// → message: 'Multiple errors occurred (3 errors)'
// → code: 'AGGREGATE_ERROR'
// → errors: [ErrorX, ErrorX, ErrorX]

// With custom options
const batchError = ErrorX.aggregate(errors, {
message: "Batch import failed",
code: "BATCH_IMPORT_FAILED",
httpStatus: 400,
metadata: { batchId: "batch_123", failedCount: 3 },
});

// Access individual errors
for (const error of aggregate.errors) {
console.log(error.code, error.message);
// Each error preserves its chain: error.chain, error.root, error.parent
}

// Type guard
if (AggregateErrorX.isAggregateErrorX(error)) {
console.log(`Found ${error.errors.length} errors`);
error.errors.forEach((e) => console.log(e.code));
}

// Serialization (preserves all aggregated errors)
const serialized = aggregate.toJSON();
const restored = AggregateErrorX.fromJSON(serialized);
```

#### AggregateErrorX Properties

| Property | Type | Description |
| -------- | ---- | ----------- |
| `errors` | `readonly ErrorX[]` | Array of all aggregated errors |
| _...inherited_ | | All ErrorX properties (message, code, metadata, etc.) |

#### Static Methods

| Method | Description |
| ------ | ----------- |
| `ErrorX.aggregate(errors, opts?)` | Create an AggregateErrorX from an array of errors |
| `AggregateErrorX.isAggregateErrorX(value)` | Type guard to check if value is an AggregateErrorX |
| `AggregateErrorX.fromJSON(serialized)` | Deserialize back to AggregateErrorX instance |

---

## Custom Error Classes
Expand Down Expand Up @@ -685,6 +747,64 @@ const resolver = new ErrorXResolver<MyConfig, MyResult>({

---

## Performance

ErrorX is designed to be fast enough for production use while providing rich error handling capabilities. Here are the key performance characteristics:

### Benchmarks

Run benchmarks locally with `pnpm bench`. Results from a typical run (Apple M2):

| Operation | ops/sec | Notes |
|-----------|---------|-------|
| `new Error()` (native) | ~525k | Baseline comparison |
| `new ErrorX()` | ~38k | ~14x slower than native Error |
| `new ErrorX(options)` | ~40k | Similar to basic ErrorX |
| `ErrorX.from(ErrorX)` | ~21M | Passthrough is extremely fast |
| `ErrorX.from(Error)` | ~32k | Converts native errors |
| `toJSON()` (simple) | ~5.4M | Very fast serialization |
| `toJSON()` (with chain) | ~1.4M | Chain adds overhead |
| `fromJSON()` (simple) | ~32k | Deserialization |
| `isErrorX()` | ~21M | Near-instant type guard |
| `aggregate()` (3 errors) | ~30k | Aggregation overhead |

### Performance Characteristics

**Error Creation (~38k ops/sec)**
- Creating an ErrorX is ~14x slower than native `Error` due to stack cleaning, timestamp generation, and chain management
- Adding metadata or httpStatus has negligible impact
- Adding a cause (chaining) reduces performance by ~2x due to chain flattening

**Serialization (toJSON)**
- Simple errors: ~5.4M ops/sec (extremely fast)
- With metadata: ~346k ops/sec (JSON serialization overhead)
- With error chain: ~1.4M ops/sec (iterates chain)

**Deserialization (fromJSON)**
- ~32k ops/sec regardless of metadata
- Chain reconstruction adds ~3x overhead per chained error

**Type Guards**
- `isErrorX()` and `isAggregateErrorX()`: ~21M ops/sec (instant)
- `isErrorXOptions()`: ~15M ops/sec (object key checking)

**Memory Considerations**
- Deep chains (50+ levels) process at ~343 ops/sec for full create/serialize/deserialize cycle
- Large aggregates (100 errors) process at ~135 ops/sec
- No memory leaks detected in chain or aggregate handling

### When to Use ErrorX

ErrorX is suitable for:
- Application-level error handling (not hot loops)
- API error responses
- Error logging and monitoring
- Domain error modeling

For performance-critical code paths (>100k errors/sec), consider using native `Error` and converting to `ErrorX` at boundaries.

---

## UI Messages

User-friendly messages are provided separately from error presets. This allows errors to remain technical while UI messages can be managed independently (e.g., for i18n).
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@types/node": "^24.6.0",
"@vitest/coverage-v8": "^3.2.4",
"husky": "^9.1.7",
"i18next": "^25.8.0",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
Expand Down Expand Up @@ -52,6 +53,7 @@
"api-docs": "api-extractor run --local",
"api-docs:build": "pnpm build && mkdir -p ./etc && pnpm api-docs && pnpm api-docs:markdown && rm -rf ./etc ./temp",
"api-docs:markdown": "npx @microsoft/api-documenter markdown -i temp -o docs",
"bench": "vitest bench",
"build": "tsup",
"check": "biome check .",
"dev": "tsup --watch",
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading