Skip to content

Commit

Permalink
Serialize err.cause in a nice way when possible (#78)
Browse files Browse the repository at this point in the history
* Serialize `err.cause` in a nice way when possible

* Self-host pony-cause helpers

* Add pony-cause banner
  • Loading branch information
voxpelli authored Nov 24, 2021
1 parent 1debec7 commit 49b6b3d
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 3 deletions.
113 changes: 113 additions & 0 deletions lib/err-helpers.js
Original file line number Diff line number Diff line change
@@ -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<Error>} 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<Error>} 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
}
8 changes: 5 additions & 3 deletions lib/err.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions test/err.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit 49b6b3d

Please sign in to comment.