diff --git a/packages/@n8n/client-oauth2/.eslintrc.js b/packages/@n8n/client-oauth2/.eslintrc.js index 825e65949a..c3fe283453 100644 --- a/packages/@n8n/client-oauth2/.eslintrc.js +++ b/packages/@n8n/client-oauth2/.eslintrc.js @@ -10,5 +10,6 @@ module.exports = { rules: { '@typescript-eslint/consistent-type-imports': 'error', + 'n8n-local-rules/no-plain-errors': 'off', }, }; diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts index 2fc5912d0b..ed6bfb188e 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts @@ -1,4 +1,4 @@ -import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { IBinaryData, IDataObject, @@ -105,7 +105,9 @@ async function getChainPromptTemplate( if (!messageClass) { // eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown - throw new Error(`Invalid message type "${message.type}"`); + throw new ApplicationError('Invalid message type', { + extra: { messageType: message.type }, + }); } if (messageClass === HumanMessagePromptTemplate && message.messageType !== 'text') { diff --git a/packages/@n8n_io/eslint-config/base.js b/packages/@n8n_io/eslint-config/base.js index 1d17765ad4..e10f90372b 100644 --- a/packages/@n8n_io/eslint-config/base.js +++ b/packages/@n8n_io/eslint-config/base.js @@ -330,7 +330,7 @@ const config = (module.exports = { { selector: 'import', format: ['camelCase', 'PascalCase'], - } + }, ], // ---------------------------------- @@ -360,6 +360,7 @@ const config = (module.exports = { // ---------------------------------- // eslint-plugin-n8n-local-rules // ---------------------------------- + 'n8n-local-rules/no-uncaught-json-parse': 'error', 'n8n-local-rules/no-json-parse-json-stringify': 'error', @@ -370,6 +371,8 @@ const config = (module.exports = { 'n8n-local-rules/no-unused-param-in-catch-clause': 'error', + 'n8n-local-rules/no-plain-errors': 'error', + // ****************************************************************** // overrides to base ruleset // ****************************************************************** @@ -469,6 +472,7 @@ const config = (module.exports = { { files: ['test/**/*.ts', 'src/__tests__/*.ts'], rules: { + 'n8n-local-rules/no-plain-errors': 'off', 'n8n-local-rules/no-skipped-tests': process.env.NODE_ENV === 'development' ? 'warn' : 'error', diff --git a/packages/@n8n_io/eslint-config/frontend.js b/packages/@n8n_io/eslint-config/frontend.js index 3f07fb4c9d..9e202c14a5 100644 --- a/packages/@n8n_io/eslint-config/frontend.js +++ b/packages/@n8n_io/eslint-config/frontend.js @@ -52,5 +52,6 @@ module.exports = { 'vue/no-side-effects-in-computed-properties': 'warn', 'vue/no-v-text-v-html-on-component': 'warn', 'vue/return-in-computed-property': 'warn', + 'n8n-local-rules/no-plain-errors': 'off', }, }; diff --git a/packages/@n8n_io/eslint-config/local-rules.js b/packages/@n8n_io/eslint-config/local-rules.js index 56eb10d5d8..16e405a602 100644 --- a/packages/@n8n_io/eslint-config/local-rules.js +++ b/packages/@n8n_io/eslint-config/local-rules.js @@ -353,6 +353,49 @@ module.exports = { }; }, }, + + 'no-plain-errors': { + meta: { + type: 'problem', + docs: { + description: + 'Only `ApplicationError` (from the `workflow` package) or its child classes must be thrown. This ensures the error will be normalized when reported to Sentry, if applicable.', + recommended: 'error', + }, + messages: { + useApplicationError: + 'Throw an `ApplicationError` (from the `workflow` package) or its child classes.', + }, + fixable: 'code', + }, + create(context) { + return { + ThrowStatement(node) { + if (!node.argument) return; + + const isNewError = + node.argument.type === 'NewExpression' && node.argument.callee.name === 'Error'; + + const isNewlessError = + node.argument.type === 'CallExpression' && node.argument.callee.name === 'Error'; + + if (isNewError || isNewlessError) { + return context.report({ + messageId: 'useApplicationError', + node, + fix: (fixer) => + fixer.replaceText( + node, + `throw new ApplicationError(${node.argument.arguments + .map((arg) => arg.raw) + .join(', ')})`, + ), + }); + } + }, + }; + }, + }, }; const isJsonParseCall = (node) => diff --git a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts index 54133154f5..a590050525 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts @@ -117,7 +117,7 @@ export class SourceControlService { this.gitService.resetService(); return this.sourceControlPreferencesService.sourceControlPreferences; } catch (error) { - throw Error(`Failed to disconnect from source control: ${(error as Error).message}`); + throw new ApplicationError('Failed to disconnect from source control', { cause: error }); } } diff --git a/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts index 44d1c979ee..480f8e4a68 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts @@ -6,7 +6,7 @@ import { SOURCE_CONTROL_TAGS_EXPORT_FILE, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER, } from './constants'; -import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; +import { ApplicationError, type ICredentialDataDecryptedObject } from 'n8n-workflow'; import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises'; import { rmSync } from 'fs'; import { Credentials, InstanceSettings } from 'n8n-core'; @@ -138,7 +138,7 @@ export class SourceControlExportService { })), }; } catch (error) { - throw Error(`Failed to export workflows to work folder: ${(error as Error).message}`); + throw new ApplicationError('Failed to export workflows to work folder', { cause: error }); } } @@ -168,7 +168,9 @@ export class SourceControlExportService { ], }; } catch (error) { - throw Error(`Failed to export variables to work folder: ${(error as Error).message}`); + throw new ApplicationError('Failed to export variables to work folder', { + cause: error, + }); } } @@ -208,7 +210,7 @@ export class SourceControlExportService { ], }; } catch (error) { - throw Error(`Failed to export variables to work folder: ${(error as Error).message}`); + throw new ApplicationError('Failed to export variables to work folder', { cause: error }); } } @@ -280,7 +282,7 @@ export class SourceControlExportService { missingIds, }; } catch (error) { - throw Error(`Failed to export credentials to work folder: ${(error as Error).message}`); + throw new ApplicationError('Failed to export credentials to work folder', { cause: error }); } } } diff --git a/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts index e96f4b000f..3b8696b32b 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts @@ -108,7 +108,7 @@ export class SourceControlPreferencesService { }); await fsWriteFile(this.sshKeyName, keyPair.privateKey, { encoding: 'utf8', mode: 0o600 }); } catch (error) { - throw Error(`Failed to save key pair: ${(error as Error).message}`); + throw new ApplicationError('Failed to save key pair', { cause: error }); } } // update preferences only after generating key pair to prevent endless loop diff --git a/packages/node-dev/.eslintrc.js b/packages/node-dev/.eslintrc.js index 0964f88b03..cb14470366 100644 --- a/packages/node-dev/.eslintrc.js +++ b/packages/node-dev/.eslintrc.js @@ -12,5 +12,6 @@ module.exports = { rules: { 'import/order': 'off', // TODO: remove this '@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }], + 'n8n-local-rules/no-plain-errors': 'off', }, }; diff --git a/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts b/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts index 85b6eecdaf..14318acabc 100644 --- a/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts +++ b/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts @@ -259,11 +259,8 @@ export function findMatches( if (disableDotNotation && skipFields.some((field) => field.includes('.'))) { const fieldToSkip = skipFields.find((field) => field.includes('.')); - throw new Error( - `Dot notation is disabled, but field to skip comparing '${ - fieldToSkip as string - }' contains dot`, - ); + const msg = `Dot notation is disabled, but field to skip comparing '${fieldToSkip}' contains dot`; + throw new ApplicationError(msg, { level: 'warning' }); } const filteredData = { @@ -403,16 +400,18 @@ export function findMatches( export function checkMatchFieldsInput(data: IDataObject[]) { if (data.length === 1 && data[0].field1 === '' && data[0].field2 === '') { - throw new Error( + throw new ApplicationError( 'You need to define at least one pair of fields in "Fields to Match" to match on', + { level: 'warning' }, ); } for (const [index, pair] of data.entries()) { if (pair.field1 === '' || pair.field2 === '') { - throw new Error( + throw new ApplicationError( `You need to define both fields in "Fields to Match" for pair ${index + 1}, field 1 = '${pair.field1}' field 2 = '${pair.field2}'`, + { level: 'warning' }, ); } } @@ -447,7 +446,10 @@ export function checkInputAndThrowError( return get(entry.json, field, undefined) !== undefined; }); if (!isPresent) { - throw new Error(`Field '${field}' is not present in any of items in '${inputLabel}'`); + throw new ApplicationError( + `Field '${field}' is not present in any of items in '${inputLabel}'`, + { level: 'warning' }, + ); } } return input; diff --git a/packages/nodes-base/nodes/Kafka/Kafka.node.ts b/packages/nodes-base/nodes/Kafka/Kafka.node.ts index 9f34b384e2..1a971f0deb 100644 --- a/packages/nodes-base/nodes/Kafka/Kafka.node.ts +++ b/packages/nodes-base/nodes/Kafka/Kafka.node.ts @@ -14,7 +14,7 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { ApplicationError, NodeOperationError } from 'n8n-workflow'; import { generatePairedItemData } from '../../utils/utilities'; export class Kafka implements INodeType { @@ -229,7 +229,10 @@ export class Kafka implements INodeType { }; if (credentials.authentication === true) { if (!(credentials.username && credentials.password)) { - throw Error('Username and password are required for authentication'); + // eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown + throw new ApplicationError('Username and password are required for authentication', { + level: 'warning', + }); } config.sasl = { username: credentials.username as string, diff --git a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts index 17e3d0bf2d..3e6e5e52e0 100644 --- a/packages/nodes-base/nodes/TheHive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/TheHive/GenericFunctions.ts @@ -119,7 +119,7 @@ export async function prepareCustomFields( return customFields; } else if (customFieldsJson) { - throw Error('customFieldsJson value is invalid'); + throw new ApplicationError('customFieldsJson value is invalid', { level: 'warning' }); } } else if (additionalFields.customFieldsUi) { // Get Custom Field Types from TheHive