Skip to content

Commit

Permalink
feat(core): Extend error hierarchy
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tomi committed Dec 18, 2024
1 parent 5d33a6b commit 19f63fd
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 21 deletions.
38 changes: 30 additions & 8 deletions packages/core/src/error-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { NodeOptions } from '@sentry/node';
import { close } from '@sentry/node';
import type { ErrorEvent, EventHint } from '@sentry/types';
import { AxiosError } from 'axios';
import { ApplicationError, LoggerProxy, type ReportingOptions } from 'n8n-workflow';
import { ApplicationError, BaseError, LoggerProxy, type ReportingOptions } from 'n8n-workflow';
import { createHash } from 'node:crypto';
import { Service } from 'typedi';

Expand Down Expand Up @@ -113,13 +113,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 &&
Expand Down Expand Up @@ -159,4 +154,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 (error instanceof BaseError) {
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, originalException: unknown): boolean {
if (originalException instanceof ApplicationError) {
const { level, extra, tags } = originalException;
if (level === 'warning') return true;

event.level = level;
if (extra) event.extra = { ...event.extra, ...extra };
if (tags) event.tags = { ...event.tags, ...tags };
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ApplicationError } from '@/errors/application.error';
import type { ReportingOptions } from '@/errors/error.types';

import type { Functionality, IDataObject, JsonObject } from '../../Interfaces';
import { ApplicationError, type ReportingOptions } from '../application.error';

interface ExecutionBaseErrorOptions extends ReportingOptions {
cause?: Error;
Expand Down
12 changes: 5 additions & 7 deletions packages/workflow/src/errors/application.error.ts
Original file line number Diff line number Diff line change
@@ -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']>;

Expand Down
50 changes: 50 additions & 0 deletions packages/workflow/src/errors/base/base.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Event } from '@sentry/node';
import callsites from 'callsites';

import type { ErrorTags, ErrorLevel, ReportingOptions } from '@/errors/error.types';

export type BaseErrorOptions = { description?: undefined | null } & ErrorOptions & ReportingOptions;

/**
* Base class for all errors
*/
export abstract class BaseError extends Error {
readonly level: ErrorLevel;

readonly tags: ErrorTags;

readonly shouldReport: boolean;

readonly description: string | null | undefined;

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 {}
}
}
23 changes: 23 additions & 0 deletions packages/workflow/src/errors/base/operational.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { BaseErrorOptions } from '@/errors/base/base.error';
import { BaseError } from '@/errors/base/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 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);
}
}
23 changes: 23 additions & 0 deletions packages/workflow/src/errors/base/unexpected.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { BaseErrorOptions } from '@/errors/base/base.error';
import { BaseError } from '@/errors/base/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);
}
}
26 changes: 26 additions & 0 deletions packages/workflow/src/errors/base/user.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { BaseErrorOptions } from '@/errors/base/base.error';
import { BaseError } from '@/errors/base/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 they’re 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);
}
}
13 changes: 13 additions & 0 deletions packages/workflow/src/errors/error.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Event } from '@sentry/node';

export type ErrorLevel = 'warning' | 'error' | 'fatal' | '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'];
};
7 changes: 6 additions & 1 deletion packages/workflow/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { ApplicationError, type ReportingOptions } from './application.error';
export type * from './error.types';
export { BaseError, 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 { ApplicationError } from './application.error';
export { ExpressionError } from './expression.error';
export { CredentialAccessError } from './credential-access-error';
export { ExecutionCancelledError } from './execution-cancelled.error';
Expand Down
4 changes: 2 additions & 2 deletions packages/workflow/src/errors/node-api.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions packages/workflow/src/errors/trigger-close.error.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 19f63fd

Please sign in to comment.