Skip to content

Commit

Permalink
feat: add inverse mode of stacktrace
Browse files Browse the repository at this point in the history
  • Loading branch information
stas-nc committed Oct 2, 2024
1 parent 1c434a4 commit ddf4e1f
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
22 changes: 17 additions & 5 deletions main/ErrorExtender.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
Expand All @@ -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;
24 changes: 24 additions & 0 deletions main/ErrorExtenderTest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});

0 comments on commit ddf4e1f

Please sign in to comment.