npm package: @serejke/eslint-plugin-no-new-error
Description: ESLint rule to enforce BaseError usage with static messages
Author: Sergei Patrikeev
Latest version: 1.2.0

# @serejke/eslint-plugin-no-new-error

ESLint plugin that enforces the use of BaseError subclasses with static messages instead of the native `Error` constructor.

## Why?

This plugin enforces a strict error handling pattern that ensures:

- **JSON Serializability**: All errors can be serialized/deserialized (critical for Temporal.io workflows)
- **Consistent Error Messages**: Static messages are greppable and predictable
- **Rich Context**: Dynamic data is stored in typed context objects
- **Type Safety**: Error context is strongly typed

## Installation

```bash
npm install --save-dev @serejke/eslint-plugin-no-new-error
# or
pnpm add -D @serejke/eslint-plugin-no-new-error
# or
yarn add -D @serejke/eslint-plugin-no-new-error
```

## Usage

Add the plugin to your `eslint.config.mjs` (ESLint 9+ flat config):

```javascript
import noNewError from '@serejke/eslint-plugin-no-new-error';

export default [
  {
    files: ['src/**/*.ts'],
    plugins: {
      'no-new-error': noNewError,
    },
    rules: {
      'no-new-error/no-new-error': 'error',
    },
  },
];
```

## Rule Details

### ❌ Incorrect

```typescript
// Using new Error()
throw new Error('Something went wrong');
throw new Error(`User ${id} not found`);
throw new Error('Failed: ' + reason);

// Dynamic messages in BaseError
throw new BaseError(`User ${userId} not found`);
throw new BaseError('Token: ' + tokenAddress);

const msg = 'Dynamic message';
throw new BaseError(msg);
```

### ✅ Correct

```typescript
// Define error with static message and typed context
export type UserNotFoundErrorContext = { userId: string };

export class UserNotFoundError extends BaseError<UserNotFoundErrorContext> {
  constructor(context: UserNotFoundErrorContext, cause?: Error) {
    super('User not found', { context, cause });
  }
}

// Usage
throw new UserNotFoundError({ userId: '123' });
```

```typescript
// Error without context
export class NetworkError extends BaseError {
  constructor(cause?: Error) {
    super('Network request failed', { cause });
  }
}

throw new NetworkError();
```

## Configuration

The rule accepts an options object:

```typescript
{
  baseErrorImportPath?: string;      // Default: '@serejke/ts-ninja-error'
  allowInTests?: boolean;             // Default: true
  testFilePatterns?: string[];        // Default: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts']
  allowedErrorClasses?: string[];     // Default: []
}
```

### Example with options

```javascript
export default [
  {
    files: ['src/**/*.ts'],
    plugins: {
      'no-new-error': noNewError,
    },
    rules: {
      'no-new-error/no-new-error': [
        'error',
        {
          baseErrorImportPath: '@/lib/errors/base-error',
          allowInTests: true,
          testFilePatterns: ['tests/**/*.ts', '**/*.test.ts'],
          allowedErrorClasses: ['CustomAllowedError'],
        },
      ],
    },
  },
];
```

### Allow in test files

By default, `new Error()` is allowed in test files:

```typescript
// ✅ Allowed in test files
test('handles errors', () => {
  const mockError = new Error('Test error');
  expect(() => fn(mockError)).toThrow();
});
```

To disable this behavior:

```javascript
rules: {
  'no-new-error/no-new-error': ['error', { allowInTests: false }]
}
```

## Pattern Explained

### BaseError Class

The pattern assumes you have a BaseError class similar to:

```typescript
import { JSONObject } from '@serejke/ts-ninja-plain-object';

export class BaseError<Context extends JSONObject = JSONObject> extends Error {
  public readonly context?: Context;
  public readonly originalCause: Error | undefined;

  constructor(message: string, options: { cause?: Error; context?: Context } = {}) {
    super(message);
    this.originalCause = options.cause;
    this.context = options.context;
    this.name = this.constructor.name;
  }
}
```

### Creating Custom Errors

1. Define a type for your error context:

```typescript
export type TransactionFailedErrorContext = {
  transactionId: string;
  reason: string;
  amount?: number;
};
```

2. Create an error class that extends BaseError:

```typescript
export class TransactionFailedError extends BaseError<TransactionFailedErrorContext> {
  constructor(context: TransactionFailedErrorContext, cause?: Error) {
    super('Transaction failed', { context, cause });
  }
}
```

3. Use it with rich, typed context:

```typescript
throw new TransactionFailedError({
  transactionId: 'tx_123',
  reason: 'Insufficient funds',
  amount: 100,
});
```

### Benefits

- **Static Messages**: Easy to search and track
- **Typed Context**: IDE autocomplete and type checking
- **Serializable**: Works with Temporal.io and other serialization requirements
- **Cause Chain**: Preserve original errors
- **Structured Data**: JSON-compatible context

## License

MIT

## Author

Sergei Patrikeev <so.patrikeev@gmail.com>