Skip to content

Commit

Permalink
util: always visualize cause property in errors during inspection
Browse files Browse the repository at this point in the history
While inspecting errors, always visualize the cause. That property
is non-enumerable by default while being useful in general for
debugging.

Duplicated stack frames are hidden.

Signed-off-by: Ruben Bridgewater <[email protected]>
  • Loading branch information
BridgeAR committed Nov 28, 2021
1 parent 265a47d commit a4f23f6
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 13 deletions.
85 changes: 72 additions & 13 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,25 +1162,57 @@ 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 = Math.min(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) {
frames.splice(offset + 1, len - 2,
ctx.stylize(` ... ${len - 2} lines matching cause stack trace ...`, '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) &&
Expand All @@ -1206,6 +1238,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)
Expand All @@ -1217,7 +1276,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])) {
Expand Down
21 changes: 21 additions & 0 deletions test/message/util-inspect-error-cause.js
Original file line number Diff line number Diff line change
@@ -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);
});
68 changes: 68 additions & 0 deletions test/message/util-inspect-error-cause.out
Original file line number Diff line number Diff line change
@@ -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 *
}
}
}

0 comments on commit a4f23f6

Please sign in to comment.