Skip to content

Xvezda/eslint-plugin-explicit-exceptions

Repository files navigation

eslint-plugin-explicit-exceptions

NPM Version Coverage Test

Code.mp4

Just as Java’s throws keyword does, enforcing the use of JSDoc’s @throws tag to explicitly specify which exceptions a function can throw to solve unpredictable propagation of exceptions happening which also known as a JavaScript's "hidden exceptions".

Features

  • Reports and provides fixes for throwable functions that are not annotated with @throws.
  • Reports and provides fixes for async functions and Promise rejections.
  • Verifies that the exception types match the documented types.

Examples

For functions that propagate exceptions to the caller because they didn’t handle exceptions, this plugin enforces the use of a @throws comment.

// ❌ Error - no `@throws` tag
function foo() {
  throw new RangeError();
}

// ✅ OK - no exception propagation
function bar() {
  try {
    throw new TypeError();
  } catch {}
}

// ❌ Error
function baz() {
  maybeThrow();
}

// ✅ OK
/** @throws {Error} */
function qux() {
  maybeThrow();
}

It also leverages these comments for type checking, helping ensure that errors are handled safely.

// ❌ Error - type mismatch
/** @throws {TypeError} */
function foo() {
  throw new RangeError();
}

// ✅ OK
/**
 * @throws {TypeError}
 * @throws {RangeError}
 */
function bar() {
  maybeThrowTypeError();
  maybeThrowRangeError();
}

// ✅ OK
/** @throws {number} */
function baz() {
  throw 42;
}

// ✅ OK
/**
 * @throws {"error"}
 */
function qux() {
  throw 'error';
}

For error classes, since TypeScript uses duck typing for type checking, this plugin treats inherited error classes as different types. However, documenting a common parent class is permitted.

// ✅ OK
/** @throws {RangeError | TypeError} */
function foo() {
  maybeThrowRangeError();
  maybeThrowTypeError();
}

// ✅ OK
/** @throws {Error} */
function bar() {
  maybeThrowRangeError();
  maybeThrowTypeError();
}

To clearly distinguish between a synchronous throw and an asynchronous promise rejection, this plugin requires that promise rejections be documented in the special form of Promise<Error>.

/**
 * @throws {Promise<Error>}
 */
function foo() {
  return new Promise((resolve, reject) => reject(new Error()));
}

/**
 * @throws {Promise<TypeError | RangeError>}
 */
async function bar() {
  if (randomBool()) {
    throw new TypeError();  // This becomes promise rejection
  } else {
    return maybeThrowRangeError();
  }
}

For more examples, check out examples directory and rules below.

Rules

Usage

Install dependencies

# https://typescript-eslint.io/getting-started/#step-1-installation
npm install --save-dev eslint @eslint/js typescript typescript-eslint

Install plugin

npm install --save-dev eslint-plugin-explicit-exceptions

Warning

These packages are experimental.

Install custom types for better built-in, libraries lint support.

# For @types/*, i.e. @types/node
npm install --save-dev @types-with-exceptions/node
# For built-in lib replacement
npm install --save-dev @types-with-exceptions/lib

tsconfig.json

 {
    // ...
+   "typeRoots": [
+     "node_modules/@types",
+     "node_modules/@types-with-exceptions"
+   ],
+   "libReplacement": true,
    // ...
 }

Visit https://github.com/Xvezda/types-with-exceptions to see more.

Note

I'm working on documentation for frequently used APIs.
https://github.com/Xvezda/types-with-exceptions
However, it's still not usable. As a temporary workaround, you can extend the type interfaces in your own type definitions.

// e.g. Promise.reject()
interface PromiseConstructor {
  /**
   * @throws {Promise<unknown>}
   */
  reject(reason?: any): Promise<unknown>;
}

Create eslint.config.mjs

// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import explicitExceptionsLint from 'eslint-plugin-explicit-exceptions';

export default tseslint.config(
  eslint.configs.recommended,
  tseslint.configs.recommended,
  explicitExceptionsLint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
);

For legacy, .eslintrc.json

{
  "extends": [
    "eslint:recommended",
    "plugin:eslint-plugin-explicit-exceptions/recommended-type-checked-legacy"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "parserOptions": {
    "projectService": true
  },
  "root": true
}

This project uses TypeScript and typescript-eslint to leverage type information. To prevent errors or bugs caused by incorrect type data, it is recommended to set the tsconfig.json "strict" option to true.

Check out typescript-eslint for more information if you having an issue with configuring.

License

MIT License