diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 4936ab761ddccb..854b19918c16cd 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1162,25 +1162,58 @@ function getFunctionBase(value, constructor, tag) { return base; } -function formatError(err, constructor, tag, ctx, keys) { - const name = err.name != null ? String(err.name) : 'Error'; - let len = name.length; - let stack = err.stack ? String(err.stack) : ErrorPrototypeToString(err); +function identicalSequenceRange(a, b) { + for (let i = 0; i < a.length - 3; i++) { + // Find the first entry of b that matches the current entry of a. + const pos = b.indexOf(a[i]); + if (pos !== -1) { + const rest = b.length - pos; + if (rest > 3) { + let len = 1; + const maxLen = MathMin(a.length - i, rest); + // Count the number of consecutive entries. + while (maxLen > len && a[i + len] === b[pos + len]) { + len++; + } + if (len > 3) { + return { len, offset: i }; + } + } + } + } - // Do not "duplicate" error properties that are already included in the output - // otherwise. - if (!ctx.showHidden && keys.length !== 0) { - for (const name of ['name', 'message', 'stack']) { - const index = keys.indexOf(name); - // Only hide the property in case it's part of the original stack - if (index !== -1 && stack.includes(err[name])) { - keys.splice(index, 1); + return { len: 0, offset: 0 }; +} + +function getStackString(error) { + return error.stack ? String(error.stack) : ErrorPrototypeToString(error); +} + +function getStackFrames(ctx, err, stack) { + const frames = stack.split('\n'); + + // Remove stack frames identical to frames in cause. + if (err.cause) { + const causeStack = getStackString(err.cause); + const causeStackStart = causeStack.indexOf('\n at'); + if (causeStackStart !== -1) { + const causeFrames = causeStack.slice(causeStackStart + 1).split('\n'); + const { len, offset } = identicalSequenceRange(frames, causeFrames); + if (len > 0) { + const skipped = len - 2; + const msg = ` ... ${skipped} lines matching cause stack trace ...`; + frames.splice(offset + 1, skipped, ctx.stylize(msg, 'undefined')); } } } + return frames; +} +function improveStack(stack, constructor, name, tag) { // A stack trace may contain arbitrary data. Only manipulate the output // for "regular errors" (errors that "look normal") for now. + let len = name.length; + if (constructor === null || (name.endsWith('Error') && stack.startsWith(name) && @@ -1206,6 +1239,33 @@ function formatError(err, constructor, tag, ctx, keys) { } } } + return stack; +} + +function removeDuplicateErrorKeys(ctx, keys, err, stack) { + if (!ctx.showHidden && keys.length !== 0) { + for (const name of ['name', 'message', 'stack']) { + const index = keys.indexOf(name); + // Only hide the property in case it's part of the original stack + if (index !== -1 && stack.includes(err[name])) { + keys.splice(index, 1); + } + } + } +} + +function formatError(err, constructor, tag, ctx, keys) { + const name = err.name != null ? String(err.name) : 'Error'; + let stack = getStackString(err); + + removeDuplicateErrorKeys(ctx, keys, err, stack); + + if (err.cause && (keys.length === 0 || !keys.includes('cause'))) { + keys.push('cause'); + } + + stack = improveStack(stack, constructor, name, tag); + // Ignore the error message if it's contained in the stack. let pos = (err.message && stack.indexOf(err.message)) || -1; if (pos !== -1) @@ -1217,7 +1277,7 @@ function formatError(err, constructor, tag, ctx, keys) { } else if (ctx.colors) { // Highlight userland code and node modules. let newStack = stack.slice(0, stackStart); - const lines = stack.slice(stackStart + 1).split('\n'); + const lines = getStackFrames(ctx, err, stack.slice(stackStart + 1)); for (const line of lines) { const core = line.match(coreModuleRegExp); if (core !== null && NativeModule.exists(core[1])) { diff --git a/test/message/util-inspect-error-cause.js b/test/message/util-inspect-error-cause.js new file mode 100644 index 00000000000000..109812fc3815ea --- /dev/null +++ b/test/message/util-inspect-error-cause.js @@ -0,0 +1,21 @@ +'use strict'; + +require('../common'); + +class FoobarError extends Error { + status = 'Feeling good'; +} + +const cause1 = new TypeError('Inner error'); +const cause2 = new FoobarError('Individual message', { cause: cause1 }); +cause2.extraProperties = 'Yes!'; +const cause3 = new Error('Stack causes', { cause: cause2 }); + +process.nextTick(() => { + const error = new RangeError('New Stack Frames', { cause: cause2 }); + const error2 = new RangeError('New Stack Frames', { cause: cause3 }); + + console.log(error); + console.log(cause3); + console.log(error2); +}); diff --git a/test/message/util-inspect-error-cause.out b/test/message/util-inspect-error-cause.out new file mode 100644 index 00000000000000..1931ae822e60ec --- /dev/null +++ b/test/message/util-inspect-error-cause.out @@ -0,0 +1,68 @@ +RangeError: New Stack Frames + at * + at * { + [cause]: FoobarError: Individual message + at * + at * + ... 4 lines matching cause stack trace ... + at * { + status: 'Feeling good', + extraProperties: 'Yes!', + [cause]: TypeError: Inner error + at * + at * + at * + at * + at * + at * + at * + } +} +Error: Stack causes + at * + at * + ... 4 lines matching cause stack trace ... + at * { + [cause]: FoobarError: Individual message + at * + at * + ... 4 lines matching cause stack trace ... + at * + status: 'Feeling good', + extraProperties: 'Yes!', + [cause]: TypeError: Inner error + at * + at * + at * + at * + at * + at * + at * + } +} +RangeError: New Stack Frames + at * + at * { + [cause]: Error: Stack causes + at * + at * + ... 4 lines matching cause stack trace ... + at * { + [cause]: FoobarError: Individual message + at * + at * + ... 4 lines matching cause stack trace ... + at * { + status: 'Feeling good', + extraProperties: 'Yes!', + [cause]: TypeError: Inner error + at * + at * + at * + at * + at * + at * + at * + } + } +}