From fc5424477da2d09b0110a32643bd4a716e111a25 Mon Sep 17 00:00:00 2001 From: Garrit Franke <32395585+garritfra@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:12:51 +0200 Subject: [PATCH] feat(core): Add `require-node-api-error` ESLint rule for community nodes (no-changelog) (#28454) --- .../eslint-plugin-community-nodes/README.md | 50 ++--- .../node-class-description-icon-missing.md | 6 +- .../docs/rules/require-node-api-error.md | 62 ++++++ .../rules/require-node-description-fields.md | 2 +- .../src/plugin.ts | 2 + .../src/rules/index.ts | 2 + .../src/rules/require-node-api-error.test.ts | 199 ++++++++++++++++++ .../src/rules/require-node-api-error.ts | 90 ++++++++ 8 files changed, 386 insertions(+), 27 deletions(-) create mode 100644 packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-api-error.md create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.test.ts create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts diff --git a/packages/@n8n/eslint-plugin-community-nodes/README.md b/packages/@n8n/eslint-plugin-community-nodes/README.md index 8e5545c1d4a..b2043345f7e 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/README.md +++ b/packages/@n8n/eslint-plugin-community-nodes/README.md @@ -41,30 +41,32 @@ export default [ βœ… Set in the `recommended` configuration.\ β˜‘οΈ Set in the `recommendedWithoutN8nCloudSupport` configuration.\ πŸ”§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ -πŸ’‘ Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). +πŸ’‘ Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).\ +❌ Deprecated. -| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ | πŸ’‘ | -| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :--- | :--- | :- | :- | -| [ai-node-package-json](docs/rules/ai-node-package-json.md) | Enforce consistency between n8n.aiNodeSdkVersion and ai-node-sdk peer dependency in community node packages | βœ… β˜‘οΈ | | | | -| [cred-class-field-icon-missing](docs/rules/cred-class-field-icon-missing.md) | Credential class must have an `icon` property defined | βœ… β˜‘οΈ | | | πŸ’‘ | -| [credential-documentation-url](docs/rules/credential-documentation-url.md) | Enforce valid credential documentationUrl format (URL or lowercase alphanumeric slug) | βœ… β˜‘οΈ | | πŸ”§ | | -| [credential-password-field](docs/rules/credential-password-field.md) | Ensure credential fields with sensitive names have typeOptions.password = true | βœ… β˜‘οΈ | | πŸ”§ | | -| [credential-test-required](docs/rules/credential-test-required.md) | Ensure credentials have a credential test | βœ… β˜‘οΈ | | | πŸ’‘ | -| [icon-validation](docs/rules/icon-validation.md) | Validate node and credential icon files exist, are SVG format, and light/dark icons are different | βœ… β˜‘οΈ | | | πŸ’‘ | -| [missing-paired-item](docs/rules/missing-paired-item.md) | Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking. | βœ… β˜‘οΈ | | | | -| [no-credential-reuse](docs/rules/no-credential-reuse.md) | Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package | βœ… β˜‘οΈ | | | πŸ’‘ | -| [no-deprecated-workflow-functions](docs/rules/no-deprecated-workflow-functions.md) | Disallow usage of deprecated functions and types from n8n-workflow package | βœ… β˜‘οΈ | | | πŸ’‘ | -| [no-forbidden-lifecycle-scripts](docs/rules/no-forbidden-lifecycle-scripts.md) | Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages | βœ… β˜‘οΈ | | | | -| [no-http-request-with-manual-auth](docs/rules/no-http-request-with-manual-auth.md) | Disallow this.helpers.httpRequest() in functions that call this.getCredentials(). Use this.helpers.httpRequestWithAuthentication() instead. | βœ… β˜‘οΈ | | | | -| [no-restricted-globals](docs/rules/no-restricted-globals.md) | Disallow usage of restricted global variables in community nodes. | βœ… | | | | -| [no-restricted-imports](docs/rules/no-restricted-imports.md) | Disallow usage of restricted imports in community nodes. | βœ… | | | | -| [node-class-description-icon-missing](docs/rules/node-class-description-icon-missing.md) | **Deprecated.** Node class description must have an `icon` property defined. Use `require-node-description-fields` instead. | βœ… β˜‘οΈ | | | πŸ’‘ | -| [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` β€” use `NodeConnectionTypes` enum instead | βœ… β˜‘οΈ | | πŸ”§ | | -| [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | βœ… β˜‘οΈ | | πŸ”§ | | -| [options-sorted-alphabetically](docs/rules/options-sorted-alphabetically.md) | Enforce alphabetical ordering of options arrays in n8n node properties | | βœ… β˜‘οΈ | | | -| [package-name-convention](docs/rules/package-name-convention.md) | Enforce correct package naming convention for n8n community nodes | βœ… β˜‘οΈ | | | πŸ’‘ | -| [require-continue-on-fail](docs/rules/require-continue-on-fail.md) | Require continueOnFail() handling in execute() methods of node classes | βœ… β˜‘οΈ | | | | -| [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields | βœ… β˜‘οΈ | | | | -| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | βœ… β˜‘οΈ | | | +| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ | πŸ’‘ | ❌ | +| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :--- | :--- | :- | :- | :- | +| [ai-node-package-json](docs/rules/ai-node-package-json.md) | Enforce consistency between n8n.aiNodeSdkVersion and ai-node-sdk peer dependency in community node packages | βœ… β˜‘οΈ | | | | | +| [cred-class-field-icon-missing](docs/rules/cred-class-field-icon-missing.md) | Credential class must have an `icon` property defined | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [credential-documentation-url](docs/rules/credential-documentation-url.md) | Enforce valid credential documentationUrl format (URL or lowercase alphanumeric slug) | βœ… β˜‘οΈ | | πŸ”§ | | | +| [credential-password-field](docs/rules/credential-password-field.md) | Ensure credential fields with sensitive names have typeOptions.password = true | βœ… β˜‘οΈ | | πŸ”§ | | | +| [credential-test-required](docs/rules/credential-test-required.md) | Ensure credentials have a credential test | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [icon-validation](docs/rules/icon-validation.md) | Validate node and credential icon files exist, are SVG format, and light/dark icons are different | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [missing-paired-item](docs/rules/missing-paired-item.md) | Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking. | βœ… β˜‘οΈ | | | | | +| [no-credential-reuse](docs/rules/no-credential-reuse.md) | Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [no-deprecated-workflow-functions](docs/rules/no-deprecated-workflow-functions.md) | Disallow usage of deprecated functions and types from n8n-workflow package | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [no-forbidden-lifecycle-scripts](docs/rules/no-forbidden-lifecycle-scripts.md) | Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages | βœ… β˜‘οΈ | | | | | +| [no-http-request-with-manual-auth](docs/rules/no-http-request-with-manual-auth.md) | Disallow this.helpers.httpRequest() in functions that call this.getCredentials(). Use this.helpers.httpRequestWithAuthentication() instead. | βœ… β˜‘οΈ | | | | | +| [no-restricted-globals](docs/rules/no-restricted-globals.md) | Disallow usage of restricted global variables in community nodes. | βœ… | | | | | +| [no-restricted-imports](docs/rules/no-restricted-imports.md) | Disallow usage of restricted imports in community nodes. | βœ… | | | | | +| [node-class-description-icon-missing](docs/rules/node-class-description-icon-missing.md) | Node class description must have an `icon` property defined. Deprecated: use `require-node-description-fields` instead. | | | | πŸ’‘ | ❌ | +| [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` β€” use `NodeConnectionTypes` enum instead | βœ… β˜‘οΈ | | πŸ”§ | | | +| [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | βœ… β˜‘οΈ | | πŸ”§ | | | +| [options-sorted-alphabetically](docs/rules/options-sorted-alphabetically.md) | Enforce alphabetical ordering of options arrays in n8n node properties | | βœ… β˜‘οΈ | | | | +| [package-name-convention](docs/rules/package-name-convention.md) | Enforce correct package naming convention for n8n community nodes | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [require-continue-on-fail](docs/rules/require-continue-on-fail.md) | Require continueOnFail() handling in execute() methods of node classes | βœ… β˜‘οΈ | | | | | +| [require-node-api-error](docs/rules/require-node-api-error.md) | Require NodeApiError or NodeOperationError for error wrapping in catch blocks. Raw errors lose HTTP context in the n8n UI. | βœ… β˜‘οΈ | | | | | +| [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | βœ… β˜‘οΈ | | | | | +| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | βœ… β˜‘οΈ | | | | diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/node-class-description-icon-missing.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/node-class-description-icon-missing.md index fb7791140e8..94e8c966cfe 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/node-class-description-icon-missing.md +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/node-class-description-icon-missing.md @@ -1,11 +1,13 @@ -# Node class description must have an `icon` property defined (`@n8n/community-nodes/node-class-description-icon-missing`) +# Node class description must have an `icon` property defined. Deprecated: use `require-node-description-fields` instead (`@n8n/community-nodes/node-class-description-icon-missing`) -❌ This rule is **deprecated**. Use [`require-node-description-fields`](require-node-description-fields.md) instead. +❌ This rule is deprecated. πŸ’‘ This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). +> **Deprecated:** Use [`require-node-description-fields`](require-node-description-fields.md) instead. + ## Rule Details Validates that node classes define an `icon` property in their `description` object. Icons are required for nodes to display correctly in the n8n editor. diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-api-error.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-api-error.md new file mode 100644 index 00000000000..90b9078d1d9 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-api-error.md @@ -0,0 +1,62 @@ +# Require NodeApiError or NodeOperationError for error wrapping in catch blocks. Raw errors lose HTTP context in the n8n UI (`@n8n/community-nodes/require-node-api-error`) + +πŸ’Ό This rule is enabled in the following configs: βœ… `recommended`, β˜‘οΈ `recommendedWithoutN8nCloudSupport`. + + + +## Rule Details + +When errors are caught and re-thrown in n8n nodes, they must be wrapped in +`NodeApiError` or `NodeOperationError`. Raw re-throws and generic `Error` +constructors lose HTTP context (status code, response body, etc.) that the n8n +UI relies on to display meaningful error information to users. + +## Examples + +### Incorrect + +```js +try { + await apiRequest(); +} catch (error) { + throw error; +} +``` + +```js +try { + await apiRequest(); +} catch (error) { + throw new Error('Request failed'); +} +``` + +### Correct + +```js +try { + await apiRequest(); +} catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); +} +``` + +```js +try { + await apiRequest(); +} catch (error) { + throw new NodeOperationError(this.getNode(), 'Operation failed', { itemIndex: i }); +} +``` + +```js +try { + await apiRequest(); +} catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw new NodeApiError(this.getNode(), error as JsonObject); +} +``` diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-description-fields.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-description-fields.md index 09aa2e2a09e..09e85368028 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-description-fields.md +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/require-node-description-fields.md @@ -1,4 +1,4 @@ -# Node class description must define all required fields (`@n8n/community-nodes/require-node-description-fields`) +# Node class description must define all required fields: icon, subtitle (`@n8n/community-nodes/require-node-description-fields`) πŸ’Ό This rule is enabled in the following configs: βœ… `recommended`, β˜‘οΈ `recommendedWithoutN8nCloudSupport`. diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts index 7ca7cea4bbb..55900b92089 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts @@ -40,6 +40,7 @@ const configs = { '@n8n/community-nodes/missing-paired-item': 'error', '@n8n/community-nodes/require-community-node-keyword': 'warn', '@n8n/community-nodes/require-continue-on-fail': 'error', + '@n8n/community-nodes/require-node-api-error': 'error', '@n8n/community-nodes/require-node-description-fields': 'error', }, }, @@ -67,6 +68,7 @@ const configs = { '@n8n/community-nodes/missing-paired-item': 'error', '@n8n/community-nodes/require-community-node-keyword': 'warn', '@n8n/community-nodes/require-continue-on-fail': 'error', + '@n8n/community-nodes/require-node-api-error': 'error', '@n8n/community-nodes/require-node-description-fields': 'error', }, }, diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts index 4b77332d7f3..5437d427f85 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts @@ -20,6 +20,7 @@ import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically import { PackageNameConventionRule } from './package-name-convention.js'; import { RequireCommunityNodeKeywordRule } from './require-community-node-keyword.js'; import { RequireContinueOnFailRule } from './require-continue-on-fail.js'; +import { RequireNodeApiErrorRule } from './require-node-api-error.js'; import { RequireNodeDescriptionFieldsRule } from './require-node-description-fields.js'; import { ResourceOperationPatternRule } from './resource-operation-pattern.js'; @@ -45,5 +46,6 @@ export const rules = { 'missing-paired-item': MissingPairedItemRule, 'require-community-node-keyword': RequireCommunityNodeKeywordRule, 'require-continue-on-fail': RequireContinueOnFailRule, + 'require-node-api-error': RequireNodeApiErrorRule, 'require-node-description-fields': RequireNodeDescriptionFieldsRule, } satisfies Record; diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.test.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.test.ts new file mode 100644 index 00000000000..a2740bfba18 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.test.ts @@ -0,0 +1,199 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { RequireNodeApiErrorRule } from './require-node-api-error.js'; + +const ruleTester = new RuleTester(); + +ruleTester.run('require-node-api-error', RequireNodeApiErrorRule, { + valid: [ + { + name: 'throw NodeApiError in catch block', + code: ` +try { + await apiRequest(); +} catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); +}`, + }, + { + name: 'throw NodeOperationError in catch block', + code: ` +try { + await apiRequest(); +} catch (error) { + throw new NodeOperationError(this.getNode(), 'Operation failed', { itemIndex: i }); +}`, + }, + { + name: 'throw outside catch block (not in scope)', + code: ` +function validate(input: string) { + if (!input) { + throw new Error('Input required'); + } +}`, + }, + { + name: 'throw new Error outside catch block (not in scope)', + code: ` +throw new Error('Something went wrong');`, + }, + { + name: 'continueOnFail pattern with NodeApiError', + code: ` +try { + responseData = await apiRequest.call(this, 'POST', '/tasks', body); +} catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw new NodeApiError(this.getNode(), error as JsonObject); +}`, + }, + { + name: 'conditional handling then NodeApiError in else', + code: ` +try { + await ftp.put(data, path); +} catch (error) { + if (error.code === 553) { + await ftp.mkdir(dirPath, true); + await ftp.put(data, path); + } else { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +}`, + }, + { + name: 'throw wrapped error stored in variable', + code: ` +try { + await apiRequest(); +} catch (error) { + const wrapped = new NodeApiError(this.getNode(), error as JsonObject); + throw wrapped; +}`, + }, + { + name: 'shadowed variable with same name as catch param', + code: ` +try { + await apiRequest(); +} catch (error) { + const fn = (error: Error) => { + throw error; + }; +}`, + }, + { + name: 'no throw in catch block', + code: ` +try { + await apiRequest(); +} catch (error) { + console.error(error); +}`, + }, + { + name: 'bare re-throw in credential file (skipped)', + filename: '/path/to/MyCredential.credentials.ts', + code: ` +try { + await apiRequest(); +} catch (error) { + throw error; +}`, + }, + { + name: 'bare re-throw in .js file (skipped)', + filename: '/path/to/helper.js', + code: ` +try { + apiRequest(); +} catch (error) { + throw error; +}`, + }, + ], + invalid: [ + { + name: 'bare re-throw of caught error', + code: ` +try { + await apiRequest(); +} catch (error) { + throw error; +}`, + errors: [{ messageId: 'useNodeApiError' }], + }, + { + name: 'throw new Error in catch block', + code: ` +try { + await apiRequest(); +} catch (error) { + throw new Error('Request failed'); +}`, + errors: [ + { + messageId: 'useNodeApiErrorInsteadOfGeneric', + data: { errorClass: 'Error' }, + }, + ], + }, + { + name: 'bare re-throw after continueOnFail', + code: ` +try { + responseData = await apiRequest.call(this, 'POST', '/tasks', body); +} catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; +}`, + errors: [{ messageId: 'useNodeApiError' }], + }, + { + name: 'throw new TypeError in catch block', + code: ` +try { + JSON.parse(data); +} catch (error) { + throw new TypeError('Invalid JSON'); +}`, + errors: [ + { + messageId: 'useNodeApiErrorInsteadOfGeneric', + data: { errorClass: 'TypeError' }, + }, + ], + }, + { + name: 'bare re-throw in nested catch', + code: ` +try { + try { + await apiRequest(); + } catch (innerError) { + throw innerError; + } +} catch (outerError) { + throw new NodeApiError(this.getNode(), outerError as JsonObject); +}`, + errors: [{ messageId: 'useNodeApiError' }], + }, + { + name: 'throw named variable in catch', + code: ` +try { + await apiRequest(); +} catch (e) { + throw e; +}`, + errors: [{ messageId: 'useNodeApiError' }], + }, + ], +}); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts new file mode 100644 index 00000000000..91e1c038a66 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts @@ -0,0 +1,90 @@ +import { DefinitionType } from '@typescript-eslint/scope-manager'; +import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; + +import { isFileType } from '../utils/index.js'; +import { createRule } from '../utils/rule-creator.js'; + +const ALLOWED_ERROR_CLASSES = new Set(['NodeApiError', 'NodeOperationError']); + +function getThrowCalleeName(argument: TSESTree.Expression): string | null { + if (argument.type === AST_NODE_TYPES.NewExpression) { + if (argument.callee.type === AST_NODE_TYPES.Identifier) { + return argument.callee.name; + } + } + return null; +} + +function isInsideCatchClause(node: TSESTree.Node): boolean { + let current: TSESTree.Node | undefined = node.parent; + while (current) { + if (current.type === AST_NODE_TYPES.CatchClause) { + return true; + } + current = current.parent; + } + return false; +} + +export const RequireNodeApiErrorRule = createRule({ + name: 'require-node-api-error', + meta: { + type: 'problem', + docs: { + description: + 'Require NodeApiError or NodeOperationError for error wrapping in catch blocks. ' + + 'Raw errors lose HTTP context in the n8n UI.', + }, + messages: { + useNodeApiError: + 'Use `NodeApiError` or `NodeOperationError` instead of re-throwing raw errors. ' + + 'Example: `throw new NodeApiError(this.getNode(), error as JsonObject)`', + useNodeApiErrorInsteadOfGeneric: + 'Use `NodeApiError` or `NodeOperationError` instead of `{{ errorClass }}`. ' + + 'Example: `throw new NodeApiError(this.getNode(), error as JsonObject)`', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const isNodeFile = isFileType(context.filename, '.node.ts'); + const isHelperFile = + context.filename.endsWith('.ts') && + !isNodeFile && + !isFileType(context.filename, '.credentials.ts'); + + if (!isNodeFile && !isHelperFile) { + return {}; + } + + return { + ThrowStatement(node) { + if (!isInsideCatchClause(node)) return; + if (!node.argument) return; + + const { argument } = node; + + if (argument.type === AST_NODE_TYPES.Identifier) { + const scope = context.sourceCode.getScope(node); + const ref = scope.references.find((r) => r.identifier === argument); + const isCatchParam = + ref?.resolved?.defs.some((def) => def.type === DefinitionType.CatchClause) ?? false; + + if (isCatchParam) { + context.report({ node, messageId: 'useNodeApiError' }); + } + return; + } + + const calleeName = getThrowCalleeName(argument); + if (calleeName !== null && !ALLOWED_ERROR_CLASSES.has(calleeName)) { + context.report({ + node, + messageId: 'useNodeApiErrorInsteadOfGeneric', + data: { errorClass: calleeName }, + }); + } + }, + }; + }, +});