From ddf4e1f5a8a9fab65d055e560f5827e698fe9445 Mon Sep 17 00:00:00 2001 From: Stanislav Mishchyshyn Date: Wed, 2 Oct 2024 15:31:24 +0300 Subject: [PATCH] feat: add inverse mode of stacktrace --- README.md | 2 +- main/ErrorExtender.js | 22 +++++++++++++++++----- main/ErrorExtenderTest.spec.js | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d406f64..eab6a75 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The options (object literal keys) are as follows: 1) Creates prototype-based `Error` classes (child/subclass) : _"Extended Errors"_. 1) Those _"Extended Errors"_, accepts `cause` (`Error`); very much like how it is with Java `Exception`. -1) Appends stack of `cause` to the bottom of instantiated _"Extended Errors"_ stack. +1) Appends stack of `cause` to the bottom (or top) of instantiated _"Extended Errors"_ stack. 1) _"Extended Errors"_ constructor & argument _(w/ optional `new`)_: 1) `new ExtendedError(options)` 1) `ExtendedError(options)` diff --git a/main/ErrorExtender.js b/main/ErrorExtender.js index ff302f5..bae78dd 100644 --- a/main/ErrorExtender.js +++ b/main/ErrorExtender.js @@ -6,6 +6,8 @@ const validator = Assert.validator; const hro = require('./helpers/HiddenReadOnly'); const merge = require('./helpers/Merge'); +const CAUSED_BY_PREFIX = 'Caused by:'; + function configurePrototype(ExtendedErrorType, ParentErrorType) { hro(ExtendedErrorType, 'prototype', Object.create(ParentErrorType.prototype)); hro(ExtendedErrorType.prototype, 'constructor', ExtendedErrorType); @@ -81,17 +83,24 @@ function captureCause(target, options) { cause instanceof AggregateError ? `${cause.stack}\n ${cause.errors.map((e) => e.stack || e.toString()).join('\n ')}` : cause.stack || cause.toString(); - hro(target, 'stack', `${target.stack}\nCaused by: ${causeStack}`); + const concatenatedStackTrace = options.inverse + ? `${insert(causeStack)}\n${target.stack}` + : `${target.stack}\nCaused by: ${causeStack}`; + hro(target, 'stack', concatenatedStackTrace); } } +function insert(stack) { + return stack.replace(new RegExp(`^((?! |${CAUSED_BY_PREFIX}).+)$`, 'gm'), `${CAUSED_BY_PREFIX} $1`); +} + function captureProperties(target, options) { captureMessage(target, options); captureData(target, options); captureCause(target, options); } -function createExtendedErrorType(newErrorName, ParentErrorType, defaultMessage, defaultData) { +function createExtendedErrorType(newErrorName, ParentErrorType, defaultMessage, defaultData, inverse) { function ExtendedErrorType(options = {}) { Assert.isObject(options, '`options` must be an object literal (ie: `{}`)'); if (!(this instanceof ExtendedErrorType)) { @@ -104,7 +113,7 @@ function createExtendedErrorType(newErrorName, ParentErrorType, defaultMessage, Error.captureStackTrace(this, this.constructor); } - captureProperties(this, options); + captureProperties(this, { ...options, inverse }); } configurePrototype(ExtendedErrorType, ParentErrorType); configureName(ExtendedErrorType, newErrorName); @@ -114,12 +123,15 @@ function createExtendedErrorType(newErrorName, ParentErrorType, defaultMessage, return ExtendedErrorType; } -function extend(newErrorName, options = { parent: undefined, defaultMessage: undefined, defaultData: undefined }) { +function extend( + newErrorName, + options = { parent: undefined, defaultMessage: undefined, defaultData: undefined, inverse: false } +) { Assert.isNotBlank(newErrorName, '`newErrorName` cannot be blank'); Assert.isObject(options, '`options` must be an object literal (ie: `{}`)'); let parent = options.parent || Error; Assert.isError(parent, '`options.parent` is not a valid `Error`'); - return createExtendedErrorType(newErrorName, parent, options.defaultMessage, options.defaultData); + return createExtendedErrorType(newErrorName, parent, options.defaultMessage, options.defaultData, options.inverse); } module.exports = extend; diff --git a/main/ErrorExtenderTest.spec.js b/main/ErrorExtenderTest.spec.js index fbcde50..68798b1 100644 --- a/main/ErrorExtenderTest.spec.js +++ b/main/ErrorExtenderTest.spec.js @@ -126,4 +126,28 @@ describe(testName, function () { assert.strictEqual(stacktrace[1].substring(0, stacktrace[1].indexOf('\n')), 'AggregateError: msg'); assert.match(stacktrace[1], /^ Error: the root error/m); }); + + it('should inverse cause stacktraces', function () { + const NError = extendError('NError', { inverse: true }); + const SError = extendError('SError', { parent: NError, inverse: true }); + const rootError = new Error('the root error'); + const firstWrapperError = new SError({ c: rootError, m: 'first wrapper' }); + const lastWrapperError = new NError({ c: firstWrapperError, m: 'last wrapper' }); + assert.strictEqual(lastWrapperError.message, 'last wrapper'); + assert.strictEqual(lastWrapperError.cause.message, 'first wrapper'); + assert.strictEqual(lastWrapperError.cause.cause.message, 'the root error'); + { + const stacktrace = firstWrapperError.stack.match(/^.*Error:.*$/gm); + assert.strictEqual(stacktrace.length, 2); + assert.strictEqual(stacktrace[0], 'Caused by: Error: the root error'); + assert.strictEqual(stacktrace[1], 'SError: first wrapper'); + } + { + const stacktrace = lastWrapperError.stack.match(/^.*Error:.*$/gm); + assert.strictEqual(stacktrace.length, 3); + assert.strictEqual(stacktrace[0], 'Caused by: Error: the root error'); + assert.strictEqual(stacktrace[1], 'Caused by: SError: first wrapper'); + assert.strictEqual(stacktrace[2], 'NError: last wrapper'); + } + }); });