feat(core): Extend error hierarchy

Introduce new error classes:
- BaseError : Base class for all other errors
- UnexpectedError : Error that was caused a programmer error
- OperationalError : A transient error like timeout
- UserError : Error that was caused by user's action or input

Replace all usages of `ApplicationError` in `cli/src/commands` to the new classes.

Up next:
- Convert the `ExecutionBaseError` hierarchy to use the new base class
- Replace all usages of `ApplicationError` with the new error classes
This commit is contained in:
Tomi Turtiainen 2025-01-03 14:37:42 +02:00
parent b1940268e6
commit ca002b6830
28 changed files with 426 additions and 83 deletions

View file

@ -22,12 +22,7 @@ import type {
WorkflowExecuteMode,
INodeType,
} from 'n8n-workflow';
import {
Workflow,
WorkflowActivationError,
WebhookPathTakenError,
ApplicationError,
} from 'n8n-workflow';
import { Workflow, WorkflowActivationError, WebhookPathTakenError } from 'n8n-workflow';
import { Service } from 'typedi';
import { ActivationErrorsService } from '@/activation-errors.service';
@ -228,14 +223,10 @@ export class ActiveWorkflowManager {
* deregister those webhooks from external services.
*/
async clearWebhooks(workflowId: string) {
const workflowData = await this.workflowRepository.findOne({
const workflowData = await this.workflowRepository.findOneOrFail({
where: { id: workflowId },
});
if (workflowData === null) {
throw new ApplicationError('Could not find workflow', { extra: { workflowId } });
}
const workflow = new Workflow({
id: workflowId,
name: workflowData.name,

View file

@ -1,7 +1,7 @@
import type { PushPayload } from '@n8n/api-types';
import { ErrorReporter } from 'n8n-core';
import type { Workflow } from 'n8n-workflow';
import { ApplicationError } from 'n8n-workflow';
import { UnexpectedError } from 'n8n-workflow';
import { Service } from 'typedi';
import { CollaborationState } from '@/collaboration/collaboration.state';
@ -34,7 +34,7 @@ export class CollaborationService {
await this.handleUserMessage(event.userId, event.msg);
} catch (error) {
this.errorReporter.error(
new ApplicationError('Error handling CollaborationService push message', {
new UnexpectedError('Error handling CollaborationService push message', {
extra: {
msg: event.msg,
userId: event.userId,

View file

@ -1,6 +1,6 @@
import { SecurityConfig } from '@n8n/config';
import { Flags } from '@oclif/core';
import { ApplicationError } from 'n8n-workflow';
import { UserError } from 'n8n-workflow';
import { Container } from 'typedi';
import { RISK_CATEGORIES } from '@/security-audit/constants';
@ -48,7 +48,7 @@ export class SecurityAudit extends BaseCommand {
const hint = `Valid categories are: ${RISK_CATEGORIES.join(', ')}`;
throw new ApplicationError([message, hint].join('. '));
throw new UserError(`${message}. ${hint}`);
}
const result = await Container.get(SecurityAuditService).run(

View file

@ -9,7 +9,7 @@ import {
DataDeduplicationService,
ErrorReporter,
} from 'n8n-core';
import { ApplicationError, ensureError, sleep } from 'n8n-workflow';
import { ensureError, sleep, ConfigurationError } from 'n8n-workflow';
import { Container } from 'typedi';
import type { AbstractServer } from '@/abstract-server';
@ -143,7 +143,7 @@ export abstract class BaseCommand extends Command {
if (!isSelected && !isAvailable) return;
if (isSelected && !isAvailable) {
throw new ApplicationError(
throw new ConfigurationError(
'External storage selected but unavailable. Please make external storage available by adding "s3" to `N8N_AVAILABLE_BINARY_DATA_MODES`.',
);
}
@ -187,31 +187,31 @@ export abstract class BaseCommand extends Command {
const { host, bucket, credentials } = this.globalConfig.externalStorage.s3;
if (host === '') {
throw new ApplicationError(
throw new ConfigurationError(
'External storage host not configured. Please set `N8N_EXTERNAL_STORAGE_S3_HOST`.',
);
}
if (bucket.name === '') {
throw new ApplicationError(
throw new ConfigurationError(
'External storage bucket name not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME`.',
);
}
if (bucket.region === '') {
throw new ApplicationError(
throw new ConfigurationError(
'External storage bucket region not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION`.',
);
}
if (credentials.accessKey === '') {
throw new ApplicationError(
throw new ConfigurationError(
'External storage access key not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY`.',
);
}
if (credentials.accessSecret === '') {
throw new ApplicationError(
throw new ConfigurationError(
'External storage access secret not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET`.',
);
}

View file

@ -4,7 +4,7 @@ import fs from 'fs';
import { diff } from 'json-diff';
import pick from 'lodash/pick';
import type { IRun, ITaskData, IWorkflowExecutionDataProcess } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { jsonParse, UnexpectedError } from 'n8n-workflow';
import os from 'os';
import { sep } from 'path';
import { Container } from 'typedi';
@ -473,7 +473,7 @@ export class ExecuteBatch extends BaseCommand {
this.updateStatus();
}
} else {
throw new ApplicationError('Wrong execution status - cannot proceed');
throw new UnexpectedError('Wrong execution status - cannot proceed');
}
});
}

View file

@ -1,6 +1,6 @@
import { Flags } from '@oclif/core';
import type { IWorkflowBase, IWorkflowExecutionDataProcess } from 'n8n-workflow';
import { ApplicationError, ExecutionBaseError } from 'n8n-workflow';
import { ExecutionBaseError, UnexpectedError, UserError } from 'n8n-workflow';
import { Container } from 'typedi';
import { ActiveExecutions } from '@/active-executions';
@ -44,9 +44,8 @@ export class Execute extends BaseCommand {
}
if (flags.file) {
throw new ApplicationError(
throw new UserError(
'The --file flag is no longer supported. Please first import the workflow and then execute it using the --id flag.',
{ level: 'warning' },
);
}
@ -64,7 +63,7 @@ export class Execute extends BaseCommand {
}
if (!workflowData) {
throw new ApplicationError('Failed to retrieve workflow data for requested workflow');
throw new UserError('Failed to retrieve workflow data for requested workflow');
}
if (!isWorkflowIdValid(workflowId)) {
@ -87,7 +86,7 @@ export class Execute extends BaseCommand {
const data = await activeExecutions.getPostExecutePromise(executionId);
if (data === undefined) {
throw new ApplicationError('Workflow did not return any data');
throw new UnexpectedError('Workflow did not return any data');
}
if (data.data.resultData.error) {

View file

@ -1,7 +1,7 @@
import { Flags } from '@oclif/core';
import fs from 'fs';
import { Credentials } from 'n8n-core';
import { ApplicationError } from 'n8n-workflow';
import { UserError } from 'n8n-workflow';
import path from 'path';
import Container from 'typedi';
@ -123,7 +123,7 @@ export class ExportCredentialsCommand extends BaseCommand {
}
if (credentials.length === 0) {
throw new ApplicationError('No credentials found with specified filters');
throw new UserError('No credentials found with specified filters');
}
if (flags.separate) {

View file

@ -1,6 +1,6 @@
import { Flags } from '@oclif/core';
import fs from 'fs';
import { ApplicationError } from 'n8n-workflow';
import { UserError } from 'n8n-workflow';
import path from 'path';
import Container from 'typedi';
@ -107,7 +107,7 @@ export class ExportWorkflowsCommand extends BaseCommand {
});
if (workflows.length === 0) {
throw new ApplicationError('No workflows found with specified filters');
throw new UserError('No workflows found with specified filters');
}
if (flags.separate) {

View file

@ -5,7 +5,7 @@ import glob from 'fast-glob';
import fs from 'fs';
import { Cipher } from 'n8n-core';
import type { ICredentialsEncrypted } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { jsonParse, UserError } from 'n8n-workflow';
import { Container } from 'typedi';
import { UM_FIX_INSTRUCTION } from '@/constants';
@ -66,7 +66,7 @@ export class ImportCredentialsCommand extends BaseCommand {
}
if (flags.projectId && flags.userId) {
throw new ApplicationError(
throw new UserError(
'You cannot use `--userId` and `--projectId` together. Use one or the other.',
);
}
@ -81,7 +81,7 @@ export class ImportCredentialsCommand extends BaseCommand {
const result = await this.checkRelations(credentials, flags.projectId, flags.userId);
if (!result.success) {
throw new ApplicationError(result.message);
throw new UserError(result.message);
}
for (const credential of credentials) {
@ -202,7 +202,7 @@ export class ImportCredentialsCommand extends BaseCommand {
);
if (!Array.isArray(credentialsUnchecked)) {
throw new ApplicationError(
throw new UserError(
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
);
}
@ -252,7 +252,7 @@ export class ImportCredentialsCommand extends BaseCommand {
if (!userId) {
const owner = await this.transactionManager.findOneBy(User, { role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
userId = owner.id;
}

View file

@ -1,7 +1,7 @@
import { Flags } from '@oclif/core';
import glob from 'fast-glob';
import fs from 'fs';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { jsonParse, UserError } from 'n8n-workflow';
import { Container } from 'typedi';
import { UM_FIX_INSTRUCTION } from '@/constants';
@ -18,7 +18,7 @@ import { BaseCommand } from '../base-command';
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
if (!Array.isArray(workflows)) {
throw new ApplicationError(
throw new UserError(
'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
);
}
@ -29,7 +29,7 @@ function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IW
!Object.prototype.hasOwnProperty.call(workflow, 'nodes') ||
!Object.prototype.hasOwnProperty.call(workflow, 'connections')
) {
throw new ApplicationError('File does not seem to contain valid workflows.');
throw new UserError('File does not seem to contain valid workflows.');
}
}
}
@ -80,7 +80,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
}
if (flags.projectId && flags.userId) {
throw new ApplicationError(
throw new UserError(
'You cannot use `--userId` and `--projectId` together. Use one or the other.',
);
}
@ -92,7 +92,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const result = await this.checkRelations(workflows, flags.projectId, flags.userId);
if (!result.success) {
throw new ApplicationError(result.message);
throw new UserError(result.message);
}
this.logger.info(`Importing ${workflows.length} workflows...`);
@ -219,7 +219,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
if (!userId) {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
userId = owner.id;
}

View file

@ -1,7 +1,7 @@
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { Flags } from '@oclif/core';
import { ApplicationError } from 'n8n-workflow';
import { UserError } from 'n8n-workflow';
import Container from 'typedi';
import { UM_FIX_INSTRUCTION } from '@/constants';
@ -56,7 +56,7 @@ export class Reset extends BaseCommand {
Number(!!flags.deleteWorkflowsAndCredentials);
if (numberOfOptions !== 1) {
throw new ApplicationError(wrongFlagsError);
throw new UserError(wrongFlagsError);
}
const owner = await this.getOwner();
@ -71,13 +71,13 @@ export class Reset extends BaseCommand {
// Migrate all workflows and credentials to another project.
if (flags.projectId ?? flags.userId) {
if (flags.userId && ldapIdentities.some((i) => i.userId === flags.userId)) {
throw new ApplicationError(
throw new UserError(
`Can't migrate workflows and credentials to the user with the ID ${flags.userId}. That user was created via LDAP and will be deleted as well.`,
);
}
if (flags.projectId && personalProjectIds.includes(flags.projectId)) {
throw new ApplicationError(
throw new UserError(
`Can't migrate workflows and credentials to the project with the ID ${flags.projectId}. That project is a personal project belonging to a user that was created via LDAP and will be deleted as well.`,
);
}
@ -132,7 +132,7 @@ export class Reset extends BaseCommand {
const project = await Container.get(ProjectRepository).findOneBy({ id: projectId });
if (project === null) {
throw new ApplicationError(`Could not find the project with the ID ${projectId}.`);
throw new UserError(`Could not find the project with the ID ${projectId}.`);
}
return project;
@ -142,7 +142,7 @@ export class Reset extends BaseCommand {
const project = await Container.get(ProjectRepository).getPersonalProjectForUser(userId);
if (project === null) {
throw new ApplicationError(
throw new UserError(
`Could not find the user with the ID ${userId} or their personalProject.`,
);
}
@ -150,7 +150,7 @@ export class Reset extends BaseCommand {
return project;
}
throw new ApplicationError(wrongFlagsError);
throw new UserError(wrongFlagsError);
}
async catch(error: Error): Promise<void> {
@ -161,7 +161,7 @@ export class Reset extends BaseCommand {
private async getOwner() {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return owner;

View file

@ -1,5 +1,5 @@
import { Flags } from '@oclif/core';
import { ApplicationError } from 'n8n-workflow';
import { UserError } from 'n8n-workflow';
import { Container } from 'typedi';
import { ActiveExecutions } from '@/active-executions';
@ -84,9 +84,7 @@ export class Webhook extends BaseCommand {
async run() {
if (this.globalConfig.multiMainSetup.enabled) {
throw new ApplicationError(
'Webhook process cannot be started when multi-main setup is enabled.',
);
throw new UserError('Webhook process cannot be started when multi-main setup is enabled.');
}
const { ScalingService } = await import('@/scaling/scaling.service');

View file

@ -2,7 +2,12 @@ import type { NodeOptions } from '@sentry/node';
import { close } from '@sentry/node';
import type { ErrorEvent, EventHint } from '@sentry/types';
import { AxiosError } from 'axios';
import { ApplicationError, ExecutionCancelledError, type ReportingOptions } from 'n8n-workflow';
import {
ApplicationError,
isBaseError,
ExecutionCancelledError,
type ReportingOptions,
} from 'n8n-workflow';
import { createHash } from 'node:crypto';
import { Service } from 'typedi';
@ -29,11 +34,15 @@ export class ErrorReporter {
const context = executionId ? ` (execution ${executionId})` : '';
do {
const msg = [
e.message + context,
e instanceof ApplicationError && e.level === 'error' && e.stack ? `\n${e.stack}\n` : '',
].join('');
const meta = e instanceof ApplicationError ? e.extra : undefined;
let stack = '';
let meta = undefined;
if (e instanceof ApplicationError || isBaseError(e)) {
if (e.level === 'error' && e.stack) {
stack = `\n${e.stack}\n`;
}
meta = e.extra;
}
const msg = [e.message + context, stack].join('');
this.logger.error(msg, meta);
e = e.cause as Error;
} while (e);
@ -117,13 +126,8 @@ export class ErrorReporter {
return null;
}
if (originalException instanceof ApplicationError) {
const { level, extra, tags } = originalException;
if (level === 'warning') return null;
event.level = level;
if (extra) event.extra = { ...event.extra, ...extra };
if (tags) event.tags = { ...event.tags, ...tags };
}
if (this.handleBaseError(event, originalException)) return null;
if (this.handleApplicationError(event, originalException)) return null;
if (
originalException instanceof Error &&
@ -164,4 +168,31 @@ export class ErrorReporter {
if (typeof e === 'string') return new ApplicationError(e);
return;
}
/** @returns Whether the error should be dropped */
private handleBaseError(event: ErrorEvent, error: unknown): boolean {
if (isBaseError(error)) {
if (!error.shouldReport) return true;
event.level = error.level;
if (error.extra) event.extra = { ...event.extra, ...error.extra };
if (error.tags) event.tags = { ...event.tags, ...error.tags };
}
return false;
}
/** @returns Whether the error should be dropped */
private handleApplicationError(event: ErrorEvent, error: unknown): boolean {
if (error instanceof ApplicationError) {
const { level, extra, tags } = error;
if (level === 'warning') return true;
event.level = level;
if (extra) event.extra = { ...event.extra, ...extra };
if (tags) event.tags = { ...event.tags, ...tags };
}
return false;
}
}

View file

@ -2,7 +2,7 @@ import { QueryFailedError } from '@n8n/typeorm';
import type { ErrorEvent } from '@sentry/types';
import { AxiosError } from 'axios';
import { mock } from 'jest-mock-extended';
import { ApplicationError } from 'n8n-workflow';
import { ApplicationError, BaseError } from 'n8n-workflow';
import { ErrorReporter } from '@/error-reporter';
import type { Logger } from '@/logging/logger';
@ -101,6 +101,43 @@ describe('ErrorReporter', () => {
const result = await errorReporter.beforeSend(event, { originalException });
expect(result).toBeNull();
});
describe('BaseError', () => {
class TestError extends BaseError {}
it('should drop errors with shouldReport false', async () => {
const originalException = new TestError('test', { shouldReport: false });
expect(await errorReporter.beforeSend(event, { originalException })).toEqual(null);
});
it('should keep events with shouldReport true', async () => {
const originalException = new TestError('test', { shouldReport: true });
expect(await errorReporter.beforeSend(event, { originalException })).toEqual(event);
});
it('should set level, extra, and tags from BaseError', async () => {
const originalException = new TestError('Test error', {
level: 'error',
extra: { foo: 'bar' },
tags: { tag1: 'value1' },
});
const testEvent = {} as ErrorEvent;
const result = await errorReporter.beforeSend(testEvent, { originalException });
expect(result).toEqual({
level: 'error',
extra: { foo: 'bar' },
tags: {
packageName: 'core',
tag1: 'value1',
},
});
});
});
});
describe('error', () => {

View file

@ -1,5 +1,6 @@
import type { Functionality, IDataObject, JsonObject } from '../../Interfaces';
import { ApplicationError, type ReportingOptions } from '../application.error';
import { ApplicationError } from '../application.error';
import type { ReportingOptions } from '../error.types';
interface ExecutionBaseErrorOptions extends ReportingOptions {
cause?: Error;

View file

@ -1,15 +1,13 @@
import type { Event } from '@sentry/node';
import callsites from 'callsites';
export type Level = 'warning' | 'error' | 'fatal' | 'info';
export type ReportingOptions = {
level?: Level;
executionId?: string;
} & Pick<Event, 'tags' | 'extra'>;
import type { ErrorLevel, ReportingOptions } from '@/errors/error.types';
/**
* @deprecated Use `UserError`, `OperationalError` or `UnexpectedError` instead.
*/
export class ApplicationError extends Error {
level: Level;
level: ErrorLevel;
readonly tags: NonNullable<Event['tags']>;

View file

@ -0,0 +1,62 @@
import type { Event } from '@sentry/node';
import callsites from 'callsites';
import type { ErrorTags, ErrorLevel, ReportingOptions } from '../error.types';
export type BaseErrorOptions = { description?: undefined | null } & ErrorOptions & ReportingOptions;
/**
* Base class for all errors
*/
export abstract class BaseError extends Error {
/**
* Error level. Defines which level the error should be logged/reported
* @default 'error'
*/
readonly level: ErrorLevel;
/**
* Whether the error should be reported to Sentry.
* @default true
*/
readonly shouldReport: boolean;
readonly description: string | null | undefined;
readonly tags: ErrorTags;
readonly extra?: Event['extra'];
readonly packageName?: string;
constructor(
message: string,
{
level = 'error',
description,
shouldReport = true,
tags = {},
extra,
...rest
}: BaseErrorOptions = {},
) {
super(message, rest);
this.level = level;
this.shouldReport = shouldReport;
this.description = description;
this.tags = tags;
this.extra = extra;
try {
const filePath = callsites()[2].getFileName() ?? '';
const match = /packages\/([^\/]+)\//.exec(filePath)?.[1];
if (match) this.tags.packageName = match;
} catch {}
}
}
export function isBaseError(error: unknown): error is BaseError {
return error instanceof BaseError;
}

View file

@ -0,0 +1,28 @@
import type { BaseErrorOptions } from './base.error';
import { BaseError } from './base.error';
export type OperationalErrorOptions = Omit<BaseErrorOptions, 'level'> & {
level?: 'info' | 'warning' | 'error';
};
/**
* Error that indicates a transient issue, like a network request failing,
* a database query timing out, etc. These are expected to happen, are
* transient by nature and should be handled gracefully.
*
* Default level: warning
* Default shouldReport: false
*/
export class OperationalError extends BaseError {
constructor(message: string, opts: OperationalErrorOptions = {}) {
opts.level = opts.level ?? 'warning';
opts.shouldReport = opts.shouldReport ?? false;
super(message, opts);
}
}
/** Convenience function to check if an error is an instance of OperationalError */
export function isOperationalError(error: Error): error is OperationalError {
return error instanceof OperationalError;
}

View file

@ -0,0 +1,28 @@
import type { BaseErrorOptions } from './base.error';
import { BaseError } from './base.error';
export type UnexpectedErrorOptions = Omit<BaseErrorOptions, 'level'> & {
level?: 'error' | 'fatal';
};
/**
* Error that indicates something is wrong in the code: logic mistakes,
* unhandled cases, assertions that fail. These are not recoverable and
* should be brought to developers' attention.
*
* Default level: error
* Default shouldReport: true
*/
export class UnexpectedError extends BaseError {
constructor(message: string, opts: UnexpectedErrorOptions = {}) {
opts.level = opts.level ?? 'error';
opts.shouldReport = opts.shouldReport ?? true;
super(message, opts);
}
}
/** Convenience function to check if an error is an instance of UnexpectedError */
export function isUnexpectedError(error: Error): error is UnexpectedError {
return error instanceof UnexpectedError;
}

View file

@ -0,0 +1,31 @@
import type { BaseErrorOptions } from './base.error';
import { BaseError } from './base.error';
export type UserErrorOptions = Omit<BaseErrorOptions, 'level'> & {
level?: 'info' | 'warning';
description?: string | null | undefined;
};
/**
* Error that indicates the user performed an action that caused an error.
* E.g. provided invalid input, tried to access a resource theyre not
* authorized to, or violates a business rule.
*
* Default level: info
* Default shouldReport: false
*/
export class UserError extends BaseError {
readonly description: string | null | undefined;
constructor(message: string, opts: UserErrorOptions = {}) {
opts.level = opts.level ?? 'info';
opts.shouldReport = opts.shouldReport ?? false;
super(message, opts);
}
}
/** Convenience function to check if an error is an instance of UserError */
export function isUserError(error: Error): error is UserError {
return error instanceof UserError;
}

View file

@ -0,0 +1,9 @@
import { UserError } from './base/user.error';
/**
* Error that indicates the user provided invalid configuration.
*
* Default level: info
* Default shouldReport: false
*/
export class ConfigurationError extends UserError {}

View file

@ -0,0 +1,14 @@
import type { Event } from '@sentry/node';
export type ErrorLevel = 'fatal' | 'error' | 'warning' | 'info';
export type ErrorTags = NonNullable<Event['tags']>;
export type ReportingOptions = {
/** Whether the error should be reported to Sentry */
shouldReport?: boolean;
level?: ErrorLevel;
tags?: ErrorTags;
extra?: Event['extra'];
executionId?: string;
};

View file

@ -1,4 +1,10 @@
export { ApplicationError, type ReportingOptions } from './application.error';
export type * from './error.types';
export { BaseError, isBaseError, type BaseErrorOptions } from './base/base.error';
export { OperationalError, type OperationalErrorOptions } from './base/operational.error';
export { UnexpectedError, type UnexpectedErrorOptions } from './base/unexpected.error';
export { UserError, type UserErrorOptions } from './base/user.error';
export { ConfigurationError } from './configuration.error';
export { ApplicationError } from './application.error';
export { ExpressionError } from './expression.error';
export { CredentialAccessError } from './credential-access-error';
export { ExecutionCancelledError } from './execution-cancelled.error';

View file

@ -5,7 +5,7 @@ import { AxiosError } from 'axios';
import { parseString } from 'xml2js';
import { NodeError } from './abstract/node.error';
import type { ReportingOptions } from './application.error';
import type { ErrorLevel } from './error.types';
import {
NO_OP_NODE_TYPE,
UNKNOWN_ERROR_DESCRIPTION,
@ -26,7 +26,7 @@ export interface NodeOperationErrorOptions {
description?: string;
runIndex?: number;
itemIndex?: number;
level?: ReportingOptions['level'];
level?: ErrorLevel;
messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node
functionality?: Functionality;
type?: string;

View file

@ -1,8 +1,9 @@
import { ApplicationError, type Level } from './application.error';
import { ApplicationError } from './application.error';
import type { ErrorLevel } from './error.types';
import type { INode } from '../Interfaces';
interface TriggerCloseErrorOptions extends ErrorOptions {
level: Level;
level: ErrorLevel;
}
export class TriggerCloseError extends ApplicationError {

View file

@ -0,0 +1,36 @@
import { BaseError } from '@/errors/base/base.error';
import { isOperationalError, OperationalError } from '@/errors/base/operational.error';
describe('OperationalError', () => {
it('should be an instance of OperationalError', () => {
const error = new OperationalError('test');
expect(error).toBeInstanceOf(OperationalError);
});
it('should be an instance of BaseError', () => {
const error = new OperationalError('test');
expect(error).toBeInstanceOf(BaseError);
});
it('should have correct defaults', () => {
const error = new OperationalError('test');
expect(error.level).toBe('warning');
expect(error.shouldReport).toBe(false);
});
it('should allow overriding the default level and shouldReport', () => {
const error = new OperationalError('test', { level: 'error', shouldReport: true });
expect(error.level).toBe('error');
expect(error.shouldReport).toBe(true);
});
});
describe('isOperationalError', () => {
it('should return true if the error is an instance of OperationalError', () => {
expect(isOperationalError(new OperationalError('test'))).toBe(true);
});
it('should return false if the error is not an instance of OperationalError', () => {
expect(isOperationalError(new Error('test'))).toBe(false);
});
});

View file

@ -0,0 +1,36 @@
import { BaseError } from '@/errors/base/base.error';
import { isUnexpectedError, UnexpectedError } from '@/errors/base/unexpected.error';
describe('UnexpectedError', () => {
it('should be an instance of UnexpectedError', () => {
const error = new UnexpectedError('test');
expect(error).toBeInstanceOf(UnexpectedError);
});
it('should be an instance of BaseError', () => {
const error = new UnexpectedError('test');
expect(error).toBeInstanceOf(BaseError);
});
it('should have correct defaults', () => {
const error = new UnexpectedError('test');
expect(error.level).toBe('error');
expect(error.shouldReport).toBe(true);
});
it('should allow overriding the default level and shouldReport', () => {
const error = new UnexpectedError('test', { level: 'fatal', shouldReport: false });
expect(error.level).toBe('fatal');
expect(error.shouldReport).toBe(false);
});
});
describe('isUnexpectedError', () => {
it('should return true if the error is an instance of UnexpectedError', () => {
expect(isUnexpectedError(new UnexpectedError('test'))).toBe(true);
});
it('should return false if the error is not an instance of UnexpectedError', () => {
expect(isUnexpectedError(new Error('test'))).toBe(false);
});
});

View file

@ -0,0 +1,37 @@
import { BaseError } from '@/errors/base/base.error';
import { isUserError } from '@/errors/base/user.error';
import { UserError } from '@/errors/base/user.error';
describe('UserError', () => {
it('should be an instance of UserError', () => {
const error = new UserError('test');
expect(error).toBeInstanceOf(UserError);
});
it('should be an instance of BaseError', () => {
const error = new UserError('test');
expect(error).toBeInstanceOf(BaseError);
});
it('should have correct defaults', () => {
const error = new UserError('test');
expect(error.level).toBe('info');
expect(error.shouldReport).toBe(false);
});
it('should allow overriding the default level and shouldReport', () => {
const error = new UserError('test', { level: 'warning', shouldReport: true });
expect(error.level).toBe('warning');
expect(error.shouldReport).toBe(true);
});
});
describe('isUserError', () => {
it('should return true if the error is an instance of UserError', () => {
expect(isUserError(new UserError('test'))).toBe(true);
});
it('should return false if the error is not an instance of UserError', () => {
expect(isUserError(new Error('test'))).toBe(false);
});
});