feat: use ES2022 native error chaining to improve error reporting (#4431)

feat: use ES2022 native error chaining
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-10-26 11:55:39 +02:00 committed by GitHub
parent 3a9684df9f
commit 1f610b90f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 69 additions and 74 deletions

View file

@ -717,8 +717,7 @@ export class ActiveWorkflowRunner {
// Run Error Workflow if defined // Run Error Workflow if defined
const activationError = new WorkflowActivationError( const activationError = new WorkflowActivationError(
`There was a problem with the trigger node "${node.name}", for that reason did the workflow had to be deactivated`, `There was a problem with the trigger node "${node.name}", for that reason did the workflow had to be deactivated`,
error, { cause: error, node },
node,
); );
this.executeErrorWorkflow(activationError, workflowData, mode); this.executeErrorWorkflow(activationError, workflowData, mode);

View file

@ -50,8 +50,11 @@ class ExternalHooksClass implements IExternalHooksClass {
const hookFile = require(hookFilePath) as IExternalHooksFileData; const hookFile = require(hookFilePath) as IExternalHooksFileData;
this.loadHooks(hookFile); this.loadHooks(hookFile);
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access throw new Error(
throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
`Problem loading external hook file "${hookFilePath}": ${error.message}`,
{ cause: error as Error },
);
} }
} }
} }

View file

@ -95,8 +95,7 @@ export class ActiveWorkflows {
throw new WorkflowActivationError( throw new WorkflowActivationError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`There was a problem activating the workflow: "${error.message}"`, `There was a problem activating the workflow: "${error.message}"`,
error, { cause: error as Error, node: triggerNode },
triggerNode,
); );
} }
} }
@ -122,8 +121,7 @@ export class ActiveWorkflows {
throw new WorkflowActivationError( throw new WorkflowActivationError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`There was a problem activating the workflow: "${error.message}"`, `There was a problem activating the workflow: "${error.message}"`,
error, { cause: error as Error, node: pollNode },
pollNode,
); );
} }
} }

View file

@ -2,7 +2,7 @@
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"lib": ["dom", "es2020"], "lib": ["dom", "es2020", "es2022.error"],
"types": ["node", "jest"], "types": ["node", "jest"],
// TODO: remove all options below this line // TODO: remove all options below this line
"noImplicitReturns": false, "noImplicitReturns": false,

View file

@ -9,6 +9,7 @@ export class ExpressionError extends ExecutionBaseError {
constructor( constructor(
message: string, message: string,
options?: { options?: {
cause?: Error;
causeDetailed?: string; causeDetailed?: string;
description?: string; description?: string;
descriptionTemplate?: string; descriptionTemplate?: string;
@ -22,7 +23,7 @@ export class ExpressionError extends ExecutionBaseError {
type?: string; type?: string;
}, },
) { ) {
super(new Error(message)); super(message, { cause: options?.cause });
if (options?.description !== undefined) { if (options?.description !== undefined) {
this.description = options.description; this.description = options.description;

View file

@ -56,29 +56,28 @@ const ERROR_STATUS_PROPERTIES = [
*/ */
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data']; const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
interface ExecutionBaseErrorOptions {
cause?: Error | JsonObject;
}
export abstract class ExecutionBaseError extends Error { export abstract class ExecutionBaseError extends Error {
description: string | null | undefined; description: string | null | undefined;
cause: Error | JsonObject;
timestamp: number; timestamp: number;
context: IDataObject = {}; context: IDataObject = {};
lineNumber: number | undefined; lineNumber: number | undefined;
constructor(error: Error | ExecutionBaseError | JsonObject) { constructor(message: string, { cause }: ExecutionBaseErrorOptions) {
super(); const options = cause instanceof Error ? { cause } : {};
super(message, options);
this.name = this.constructor.name; this.name = this.constructor.name;
this.cause = error;
this.timestamp = Date.now(); this.timestamp = Date.now();
if (error.message) { if (cause instanceof ExecutionBaseError) {
this.message = error.message as string; this.context = cause.context;
}
if (error instanceof ExecutionBaseError) {
this.context = error.context;
} }
} }
} }
@ -91,7 +90,8 @@ abstract class NodeError extends ExecutionBaseError {
node: INode; node: INode;
constructor(node: INode, error: Error | JsonObject) { constructor(node: INode, error: Error | JsonObject) {
super(error); const message = error instanceof Error ? error.message : '';
super(message, { cause: error });
this.node = node; this.node = node;
} }
@ -120,23 +120,23 @@ abstract class NodeError extends ExecutionBaseError {
* *
*/ */
protected findProperty( protected findProperty(
error: JsonObject, jsonError: JsonObject,
potentialKeys: string[], potentialKeys: string[],
traversalKeys: string[] = [], traversalKeys: string[] = [],
): string | null { ): string | null {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const key of potentialKeys) { for (const key of potentialKeys) {
const value = error[key]; const value = jsonError[key];
if (value) { if (value) {
if (typeof value === 'string') return value; if (typeof value === 'string') return value;
if (typeof value === 'number') return value.toString(); if (typeof value === 'number') return value.toString();
if (Array.isArray(value)) { if (Array.isArray(value)) {
const resolvedErrors: string[] = value const resolvedErrors: string[] = value
.map((error) => { .map((jsonError) => {
if (typeof error === 'string') return error; if (typeof jsonError === 'string') return jsonError;
if (typeof error === 'number') return error.toString(); if (typeof jsonError === 'number') return jsonError.toString();
if (this.isTraversableObject(error)) { if (this.isTraversableObject(jsonError)) {
return this.findProperty(error, potentialKeys); return this.findProperty(jsonError, potentialKeys);
} }
return null; return null;
}) })
@ -158,7 +158,7 @@ abstract class NodeError extends ExecutionBaseError {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const key of traversalKeys) { for (const key of traversalKeys) {
const value = error[key]; const value = jsonError[key];
if (this.isTraversableObject(value)) { if (this.isTraversableObject(value)) {
const property = this.findProperty(value, potentialKeys, traversalKeys); const property = this.findProperty(value, potentialKeys, traversalKeys);
if (property) { if (property) {
@ -209,35 +209,29 @@ abstract class NodeError extends ExecutionBaseError {
} }
} }
interface NodeOperationErrorOptions {
description?: string;
runIndex?: number;
itemIndex?: number;
}
/** /**
* Class for instantiating an operational error, e.g. an invalid credentials error. * Class for instantiating an operational error, e.g. an invalid credentials error.
*/ */
export class NodeOperationError extends NodeError { export class NodeOperationError extends NodeError {
lineNumber: number | undefined; lineNumber: number | undefined;
constructor( constructor(node: INode, error: Error | string, options: NodeOperationErrorOptions = {}) {
node: INode,
error: Error | string,
options?: { description?: string; runIndex?: number; itemIndex?: number },
) {
if (typeof error === 'string') { if (typeof error === 'string') {
error = new Error(error); error = new Error(error);
} }
super(node, error); super(node, error);
if (options?.description) {
this.description = options.description; this.description = options.description;
}
if (options?.runIndex !== undefined) {
this.context.runIndex = options.runIndex; this.context.runIndex = options.runIndex;
}
if (options?.itemIndex !== undefined) {
this.context.itemIndex = options.itemIndex; this.context.itemIndex = options.itemIndex;
} }
} }
}
const STATUS_CODE_MESSAGES: IStatusCodeMessages = { const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
'4XX': 'Your request is invalid or could not be processed by the service', '4XX': 'Your request is invalid or could not be processed by the service',
@ -258,6 +252,12 @@ const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information'; const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information';
interface NodeApiErrorOptions extends NodeOperationErrorOptions {
message?: string;
httpCode?: string;
parseXml?: boolean;
}
/** /**
* Class for instantiating an error in an API response, e.g. a 404 Not Found response, * Class for instantiating an error in an API response, e.g. a 404 Not Found response,
* with an HTTP error code, an error message and a description. * with an HTTP error code, an error message and a description.
@ -268,21 +268,7 @@ export class NodeApiError extends NodeError {
constructor( constructor(
node: INode, node: INode,
error: JsonObject, error: JsonObject,
{ { message, description, httpCode, parseXml, runIndex, itemIndex }: NodeApiErrorOptions = {},
message,
description,
httpCode,
parseXml,
runIndex,
itemIndex,
}: {
message?: string;
description?: string;
httpCode?: string;
parseXml?: boolean;
runIndex?: number;
itemIndex?: number;
} = {},
) { ) {
super(node, error); super(node, error);
if (error.error) { if (error.error) {

View file

@ -277,12 +277,12 @@ export class RoutingNode {
}; };
}); });
}); });
} catch (e) { } catch (error) {
throw new NodeOperationError( throw new NodeOperationError(this.node, error, {
this.node, runIndex,
`The rootProperty "${action.properties.property}" could not be found on item.`, itemIndex,
{ runIndex, itemIndex }, description: `The rootProperty "${action.properties.property}" could not be found on item.`,
); });
} }
} }
if (action.type === 'limit') { if (action.type === 'limit') {

View file

@ -1,19 +1,27 @@
import { INode } from './Interfaces'; import { INode } from './Interfaces';
import { ExecutionBaseError } from './NodeErrors'; import { ExecutionBaseError } from './NodeErrors';
interface WorkflowActivationErrorOptions {
cause?: Error;
node?: INode;
}
/** /**
* Class for instantiating an workflow activation error * Class for instantiating an workflow activation error
*/ */
export class WorkflowActivationError extends ExecutionBaseError { export class WorkflowActivationError extends ExecutionBaseError {
node: INode | undefined; node: INode | undefined;
constructor(message: string, error: Error, node?: INode) { constructor(message: string, { cause, node }: WorkflowActivationErrorOptions) {
super(error); let error = cause as Error;
if (cause instanceof ExecutionBaseError) {
error = new Error(cause.message);
error.constructor = cause.constructor;
error.name = cause.name;
error.stack = cause.stack;
}
super(message, { cause: error });
this.node = node; this.node = node;
this.cause = {
message: error.message,
stack: error.stack as string,
};
this.message = message; this.message = message;
} }
} }

View file

@ -4,7 +4,7 @@
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"target": "es2019", "target": "es2019",
"lib": ["es2019", "es2020"], "lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true, "removeComments": true,
"useUnknownInCatchVariables": true, "useUnknownInCatchVariables": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,