npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mkvisuals/eslint-plugin-throws

v0.6.1

Published

ESLint plugin that brings checked exception awareness to JavaScript/TypeScript via @throws JSDoc tags

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-throws

Usage

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',
        ignoreThrownCallees: ['JSON.stringify'],
      }],
    },
  },
];

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',
    //   ignoreThrownCallees: ['JSON.stringify'],
    // }],
  },
};

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

IncorrectfindItem 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
  }
}

Catch clause instanceof filtering

When the catch block uses instanceof to filter error types, the plugin checks whether each declared @throws type is actually handled. Types that fall through unhandled produce a warning.

IncorrectMyError is thrown but the catch only handles a different type:

/** @throws {MyError} */
function risky() { /* ... */ }

function caller() {
  try {
    risky();
  } catch (e) {
    if (e instanceof OtherError) {
      // handle OtherError
    }
    // ⚠ MyError falls through unhandled
  }
}

Correct — the catch handles MyError explicitly:

function caller() {
  try {
    risky();
  } catch (e) {
    if (e instanceof MyError) {
      // handled
    }
  }
}

Correctelse clause acts as a catch-all:

function caller() {
  try {
    risky();
  } catch (e) {
    if (e instanceof OtherError) {
      // ...
    } else {
      // catch-all handles MyError
    }
  }
}

A bare catch (e) { ... } without any instanceof filtering is treated as handling everything (no warning).

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:

  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

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

| 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). | | ignoreThrownCallees | string[] | [] | List of callee patterns (e.g. 'JSON.stringify') to skip entirely — both as throw arguments (throw JSON.stringify(x)) and as plain calls (JSON.stringify(x)). Strongly recommended for TypeScript projects: ['JSON.stringify'] — TS lib types annotate it with @throws {TypeError} and every call would otherwise warn. |

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 (and noPropagate, see below) must appear immediately after the type, before any free-form description. Everything after the recognized modifiers — or after a - separator — is treated as a normal description. Callers of a force-only function still need to handle or propagate the @throws as usual.

The noPropagate keyword

A @throws tag marked noPropagate declares that the function may throw the type — so requireThrowsAnnotation is satisfied — but callers are exempt from catching or propagating it. Useful for defensive rethrows where the type system says Error but you don't want every caller in the call chain to wear it.

/**
 * @throws {ValidationError}
 * @throws {NotFoundError}
 * @throws {Error} noPropagate - defensive rethrow in else branch
 */
function handle() {
  try {
    risky();
  } catch (e) {
    if (e instanceof ValidationError) throw e;
    else if (e instanceof NotFoundError) throw e;
    else throw e; // typed as Error — declared but not propagated to callers
  }
}

function caller() {
  handle(); // ✅ no need to catch or declare {Error}
}

noPropagate also absorbs throws inside inline callbacks: when a callback declares @throws {Type} noPropagate, throws of that type don't bubble up to the enclosing function's requireThrowsAnnotation check.

/** @throws {OuterError} force */
function doStuff() {
  someAsyncRunner(
    /** @throws {CallbackException} noPropagate */
    async () => {
      throw new CallbackException(); // ✅ doesn't force doStuff to declare it
    }
  );
}

noPropagate is composable with force in either order: @throws {Type} force noPropagate and @throws {Type} noPropagate force both work. Note that noPropagate does not auto-keep a tag from removeUnnecessaryThrows — only force does that.

ignoreThrownCallees

🔥 If you use TypeScript with type-aware linting, you almost certainly want ignoreThrownCallees: ['JSON.stringify']. TypeScript's lib type definitions annotate JSON.stringify with @throws {TypeError}, so every plain JSON.stringify(...) call in your codebase will be flagged as an unguarded throw. This is the single most common source of false positives — add it to your config and move on with your life.

ignoreThrownCallees accepts a list of callee patterns to skip in two situations:

1. Calls to functions whose @throws you don't care about (default mode):

'throws/no-uncaught-throws': ['warn', {
  ignoreThrownCallees: ['JSON.stringify'],
}]
function format(data: unknown) {
  return JSON.stringify(data); // ✅ no warning, even though TS lib says @throws {TypeError}
}

2. throw foo() patterns where the call result isn't a real exception type (requireThrowsAnnotation mode):

function formatAndThrow(data) {
  throw JSON.stringify(data); // ✅ ignored — would otherwise demand @throws {JSON}
}

Each entry is matched against the callee shape — both Object.method (single-level member expression) and bare funcName patterns are supported. Wrapping in new Error(JSON.stringify(...)) is not matched (and would correctly resolve to Error).

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 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:

/** @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