npm package: @mkvisuals/eslint-plugin-throws
Description: ESLint plugin that brings checked exception awareness to JavaScript/TypeScript via @throws JSDoc tags
Latest version: 0.3.2

# @mkvisuals/eslint-plugin-throws

Bring Java/PHP-style checked exception awareness to JavaScript and TypeScript via `@throws` JSDoc tags.

When a function is annotated with `@throws`, every call site must either be wrapped in a `try/catch` block or the calling function must itself declare `@throws` (propagation). The plugin also optionally enforces that any function containing a `throw` statement has a `@throws` annotation.

Works with **ESLint 8** (legacy eslintrc) and **ESLint 9** (flat config). No external dependencies.

## Installation

```sh
npm install --save-dev @mkvisuals/eslint-plugin-throws
```

## Usage

### ESLint 9 — flat config

```js
// eslint.config.js
import throws from '@mkvisuals/eslint-plugin-throws';

export default [
  // Use the recommended preset (warn on uncaught @throws call sites)
  ...throws.configs.recommended,
];
```

Or with the strict preset (also requires `@throws` on any function that throws):

```js
export default [
  ...throws.configs.strict,
];
```

Or configure manually with all options:

```js
import throws from '@mkvisuals/eslint-plugin-throws';

export default [
  {
    plugins: { throws },
    rules: {
      'throws/no-uncaught-throws': ['warn', {
        requireThrowsAnnotation: true,
        removeUnnecessaryThrows: true,
        fixStrategy: 'propagate',
      }],
    },
  },
];
```

### ESLint 8 — legacy eslintrc

```js
// .eslintrc.cjs
module.exports = {
  plugins: ['@mkvisuals/eslint-plugin-throws'],
  rules: {
    'throws/no-uncaught-throws': 'warn',
    // or with all options:
    // 'throws/no-uncaught-throws': ['warn', {
    //   requireThrowsAnnotation: true,
    //   removeUnnecessaryThrows: true,
    //   fixStrategy: 'propagate',
    // }],
  },
};
```

### TypeScript — cross-file resolution

By default the plugin resolves callees within the same file only. To enable **cross-file resolution** (e.g. `this.service.method()` via NestJS dependency injection), configure `@typescript-eslint/parser` with type information:

```sh
npm install --save-dev typescript-eslint
```

```js
// eslint.config.js
import throws from '@mkvisuals/eslint-plugin-throws';
import tseslint from 'typescript-eslint';

export default [
  {
    files: ['**/*.{js,ts}'],
    plugins: { throws },
    rules: {
      'throws/no-uncaught-throws': ['warn', { requireThrowsAnnotation: true }],
    },
  },
  {
    files: ['**/*.ts'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
];
```

When type information is available, the plugin uses TypeScript's type checker to resolve method calls across files and read `@throws` JSDoc tags from the target declaration. Without type information, it falls back to same-file resolution.

## Rules

### `throws/no-uncaught-throws`

Warns when a function annotated with `@throws` is called without a surrounding `try/catch` and the caller itself has no `@throws` annotation.

#### Examples

**Incorrect** — `findItem` declares `@throws` but the call is unguarded:

```js
/** @throws {NotFoundException} */
function findItem(id) { /* ... */ }

function loadUser(id) {
  const item = findItem(id); // ⚠ warning
}
```

**Correct** — wrapped in try/catch:

```js
function loadUser(id) {
  try {
    const item = findItem(id);
  } catch (error) {
    // handle it
  }
}
```

**Correct** — propagation via `@throws` on the caller:

```js
/** @throws {NotFoundException} */
function loadUser(id) {
  const item = findItem(id); // OK — caller declares @throws
}
```

#### IDE suggestions (lightbulb fixes)

Each warning offers two quick-fix suggestions:

1. **Propagate @throws to enclosing function** — adds/appends `@throws {Type}` to the caller's JSDoc
2. **Wrap in try/catch** — wraps the statement in `try { ... } catch (error) { throw error; }`

#### Options

```js
'throws/no-uncaught-throws': ['warn', {
  requireThrowsAnnotation: false,  // default
  removeUnnecessaryThrows: false,  // default
  fixStrategy: undefined,          // default (no autofix)
}]
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `requireThrowsAnnotation` | `boolean` | `false` | When `true`, warns when a function contains a `throw` statement but has no `@throws` JSDoc annotation. |
| `removeUnnecessaryThrows` | `boolean` | `false` | When `true`, warns when a function has a `@throws` annotation but does not throw or call any function annotated with `@throws`. Autofixes by removing the stale annotation. |
| `fixStrategy` | `'propagate'` \| `'try-catch'` | — | Enables `eslint --fix` for uncaught throws. `'propagate'` adds `@throws` to the calling function. `'try-catch'` wraps the call in a try/catch block. When not set, only IDE suggestions are provided (no autofix). |

#### `requireThrowsAnnotation`

With `requireThrowsAnnotation: true`:

```js
function riskyFn() {
  throw new NotFoundException(); // ⚠ missing @throws annotation
}
```

The suggestion **Add @throws annotation** will create or append the correct `@throws {NotFoundException}` tag.

#### `removeUnnecessaryThrows`

With `removeUnnecessaryThrows: true`:

```js
/** @throws {NotFoundException} */
function safeFn() {
  // no throw, no calls to @throws functions
  return 42; // ⚠ unnecessary @throws annotation
}
```

The fix removes the `@throws` lines from the JSDoc. If the JSDoc only contained `@throws`, the entire comment is removed.

##### The `force` keyword

When calling external libraries that throw but don't have `@throws` annotations (compiled code, no JSDoc), you can mark a `@throws` tag with `force` to prevent it from being flagged as unnecessary:

```ts
/**
 * @throws {ORMException} force - TypeORM save() internally throws this
 */
async save(entity: User): Promise<void> {
  await this.repository.save(entity); // ✅ no "unnecessary" warning
}
```

The `force` keyword must be the **first word** after the type. Everything after it is treated as a normal description. Callers of this function will still need to handle or propagate the `@throws` as usual.

#### `fixStrategy`

With `fixStrategy: 'propagate'`, running `eslint --fix` will automatically add `@throws` to the enclosing function:

```js
/** @throws {NotFoundException} */
function findItem(id) { /* ... */ }

// Before fix:
function loadUser(id) {
  const item = findItem(id); // ⚠ warning
}

// After eslint --fix:
/** @throws {NotFoundException} */
function loadUser(id) {
  const item = findItem(id); // ✅ fixed
}
```

With `fixStrategy: 'try-catch'`, running `eslint --fix` wraps the call instead:

```js
// After eslint --fix:
function loadUser(id) {
  try {
    const item = findItem(id); // ✅ fixed
  } catch (error) {
    throw error;
  }
}
```

## Resolution scope

The rule resolves callees within the **same file** using ESLint's scope analysis:

- Direct calls: `foo()`
- `this.method()` inside a class
- `obj.method()` for locally-defined object literals

### Cross-file resolution (TypeScript)

When `@typescript-eslint/parser` is configured with type information (`parserOptions.projectService` or `parserOptions.project`), the rule also resolves **cross-file calls**:

- `this.service.method()` — injected dependencies (e.g. NestJS DI)
- Any member expression call where TypeScript can resolve the type

The plugin reads `@throws` JSDoc tags from the resolved method declaration via TypeScript's type checker. No additional dependencies are required beyond `typescript-eslint`.

### Type-level matching

The rule compares exception types between caller and callee. Only **uncovered** exception types produce warnings:

```ts
/** @throws {NotFoundException} */
function findItem(id: string) { /* ... */ }

/** @throws {NotFoundException} */
function loadUser(id: string) {
  return findItem(id); // ✅ NotFoundException is covered
}
```

If a callee throws multiple types, only the ones not declared in the caller's `@throws` are reported:

```ts
/** @throws {NotFoundException} */
/** @throws {ForbiddenException} */
function riskyCall() { /* ... */ }

/** @throws {NotFoundException} */
function handler() {
  riskyCall(); // ⚠ ForbiddenException is not covered
}
```

### Exception inheritance (TypeScript)

When type information is available, the rule understands class inheritance. A parent exception type covers all of its subclasses:

```ts
class AppException extends Error {}
class NotFoundException extends AppException {}
class ForbiddenException extends AppException {}

/** @throws {NotFoundException} */
function findItem() { /* ... */ }

/** @throws {AppException} */
function handler() {
  findItem(); // ✅ NotFoundException extends AppException — covered
}
```

This works across files — the exception classes don't need to be imported in the calling file. The plugin resolves the inheritance chain via TypeScript's type checker.

> **Note:** Exception inheritance checking requires TypeScript with type-aware linting (see [cross-file setup](#typescript--cross-file-resolution)). In plain JavaScript, only exact type name matching is used.

## License

MIT