diff --git a/lib/err-helpers.js b/lib/err-helpers.js new file mode 100644 index 0000000..17b465f --- /dev/null +++ b/lib/err-helpers.js @@ -0,0 +1,113 @@ +'use strict' + +// ************************************************************** +// * Code initially copied/adapted from "pony-cause" npm module * +// * Please upstream improvements there * +// ************************************************************** + +/** + * @param {Error|{ cause?: unknown|(()=>err)}} err + * @returns {Error|undefined} + */ +const getErrorCause = (err) => { + if (!err) return + + /** @type {unknown} */ + // @ts-ignore + const cause = err.cause + + // VError / NError style causes + if (typeof cause === 'function') { + // @ts-ignore + const causeResult = err.cause() + + return causeResult instanceof Error + ? causeResult + : undefined + } else { + return cause instanceof Error + ? cause + : undefined + } +} + +/** + * Internal method that keeps a track of which error we have already added, to avoid circular recursion + * + * @private + * @param {Error} err + * @param {Set} seen + * @returns {string} + */ +const _stackWithCauses = (err, seen) => { + if (!(err instanceof Error)) return '' + + const stack = err.stack || '' + + // Ensure we don't go circular or crazily deep + if (seen.has(err)) { + return stack + '\ncauses have become circular...' + } + + const cause = getErrorCause(err) + + if (cause) { + seen.add(err) + return (stack + '\ncaused by: ' + _stackWithCauses(cause, seen)) + } else { + return stack + } +} + +/** + * @param {Error} err + * @returns {string} + */ +const stackWithCauses = (err) => _stackWithCauses(err, new Set()) + +/** + * Internal method that keeps a track of which error we have already added, to avoid circular recursion + * + * @private + * @param {Error} err + * @param {Set} seen + * @param {boolean} [skip] + * @returns {string} + */ +const _messageWithCauses = (err, seen, skip) => { + if (!(err instanceof Error)) return '' + + const message = skip ? '' : (err.message || '') + + // Ensure we don't go circular or crazily deep + if (seen.has(err)) { + return message + ': ...' + } + + const cause = getErrorCause(err) + + if (cause) { + seen.add(err) + + // @ts-ignore + const skipIfVErrorStyleCause = typeof err.cause === 'function' + + return (message + + (skipIfVErrorStyleCause ? '' : ': ') + + _messageWithCauses(cause, seen, skipIfVErrorStyleCause)) + } else { + return message + } +} + +/** + * @param {Error} err + * @returns {string} + */ +const messageWithCauses = (err) => _messageWithCauses(err, new Set()) + +module.exports = { + getErrorCause, + stackWithCauses, + messageWithCauses +} diff --git a/lib/err.js b/lib/err.js index 98570d5..0b6d925 100644 --- a/lib/err.js +++ b/lib/err.js @@ -2,6 +2,8 @@ module.exports = errSerializer +const { messageWithCauses, stackWithCauses } = require('./err-helpers') + const { toString } = Object.prototype const seen = Symbol('circular-ref-tag') const rawSymbol = Symbol('pino-raw-err-ref') @@ -46,12 +48,12 @@ function errSerializer (err) { _err.type = toString.call(err.constructor) === '[object Function]' ? err.constructor.name : err.name - _err.message = err.message - _err.stack = err.stack + _err.message = messageWithCauses(err) + _err.stack = stackWithCauses(err) for (const key in err) { if (_err[key] === undefined) { const val = err[key] - if (val instanceof Error) { + if (val instanceof Error && key !== 'cause') { /* eslint-disable no-prototype-builtins */ if (!val.hasOwnProperty(seen)) { _err[key] = errSerializer(val) diff --git a/test/err.test.js b/test/err.test.js index 5acc7d8..6cb7e04 100644 --- a/test/err.test.js +++ b/test/err.test.js @@ -46,6 +46,38 @@ test('serializes nested errors', function (t) { t.match(serialized.inner.stack, /err\.test\.js:/) }) +test('serializes error causes', function (t) { + t.plan(6) + const err = Error('foo') + err.cause = Error('bar') + err.cause.cause = Error('abc') + const serialized = serializer(err) + t.is(serialized.type, 'Error') + t.is(serialized.message, 'foo: bar: abc') + t.match(serialized.stack, /err\.test\.js:/) + t.match(serialized.stack, /Error: foo/) + t.match(serialized.stack, /Error: bar/) + t.match(serialized.stack, /Error: abc/) +}) + +test('serializes error causes with VError support', function (t) { + t.plan(6) + // Fake VError-style setyp + const err = Error('foo: bar') + err.cause = () => { + const err = Error('bar') + err.cause = Error('abc') + return err + } + const serialized = serializer(err) + t.is(serialized.type, 'Error') + t.is(serialized.message, 'foo: bar: abc') + t.match(serialized.stack, /err\.test\.js:/) + t.match(serialized.stack, /Error: foo/) + t.match(serialized.stack, /Error: bar/) + t.match(serialized.stack, /Error: abc/) +}) + test('prevents infinite recursion', function (t) { t.plan(4) const err = Error('foo')