@mkvisuals/eslint-plugin-throws
v0.2.1
Published
ESLint plugin that brings checked exception awareness to JavaScript/TypeScript via @throws JSDoc tags
Maintainers
Readme
@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
npm install --save-dev @mkvisuals/eslint-plugin-throwsUsage
ESLint 9 — flat config
// 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):
export default [
...throws.configs.strict,
];Or configure manually with all options:
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
// .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:
npm install --save-dev typescript-eslint// 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:
/** @throws {NotFoundException} */
function findItem(id) { /* ... */ }
function loadUser(id) {
const item = findItem(id); // ⚠ warning
}Correct — wrapped in try/catch:
function loadUser(id) {
try {
const item = findItem(id);
} catch (error) {
// handle it
}
}Correct — propagation via @throws on the caller:
/** @throws {NotFoundException} */
function loadUser(id) {
const item = findItem(id); // OK — caller declares @throws
}IDE suggestions (lightbulb fixes)
Each warning offers two quick-fix suggestions:
- Propagate @throws to enclosing function — adds/appends
@throws {Type}to the caller's JSDoc - Wrap in try/catch — wraps the statement in
try { ... } catch (error) { throw error; }
Options
'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:
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:
/** @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:
/**
* @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:
/** @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:
// 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 classobj.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:
/** @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:
/** @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:
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). In plain JavaScript, only exact type name matching is used.
License
MIT
