Skip to content

Commit a4f23f6

Browse files
committed
util: always visualize cause property in errors during inspection
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]>
1 parent 265a47d commit a4f23f6

File tree

3 files changed

+161
-13
lines changed

3 files changed

+161
-13
lines changed

lib/internal/util/inspect.js

+72-13
Original file line numberDiff line numberDiff line change
@@ -1162,25 +1162,57 @@ function getFunctionBase(value, constructor, tag) {
11621162
return base;
11631163
}
11641164

1165-
function formatError(err, constructor, tag, ctx, keys) {
1166-
const name = err.name != null ? String(err.name) : 'Error';
1167-
let len = name.length;
1168-
let stack = err.stack ? String(err.stack) : ErrorPrototypeToString(err);
1165+
function identicalSequenceRange(a, b) {
1166+
for (let i = 0; i < a.length - 3; i++) {
1167+
// Find the first entry of b that matches the current entry of a.
1168+
const pos = b.indexOf(a[i]);
1169+
if (pos !== -1) {
1170+
const rest = b.length - pos;
1171+
if (rest > 3) {
1172+
let len = 1;
1173+
const maxLen = Math.min(a.length - i, rest);
1174+
// Count the number of consecutive entries.
1175+
while (maxLen > len && a[i + len] === b[pos + len]) {
1176+
len++;
1177+
}
1178+
if (len > 3) {
1179+
return { len, offset: i };
1180+
}
1181+
}
1182+
}
1183+
}
11691184

1170-
// Do not "duplicate" error properties that are already included in the output
1171-
// otherwise.
1172-
if (!ctx.showHidden && keys.length !== 0) {
1173-
for (const name of ['name', 'message', 'stack']) {
1174-
const index = keys.indexOf(name);
1175-
// Only hide the property in case it's part of the original stack
1176-
if (index !== -1 && stack.includes(err[name])) {
1177-
keys.splice(index, 1);
1185+
return { len: 0, offset: 0 };
1186+
}
1187+
1188+
function getStackString(error) {
1189+
return error.stack ? String(error.stack) : ErrorPrototypeToString(error);
1190+
}
1191+
1192+
function getStackFrames(ctx, err, stack) {
1193+
const frames = stack.split('\n');
1194+
1195+
// Remove stack frames identical to frames in cause.
1196+
if (err.cause) {
1197+
const causeStack = getStackString(err.cause)
1198+
const causeStackStart = causeStack.indexOf('\n at');
1199+
if (causeStackStart !== -1) {
1200+
const causeFrames = causeStack.slice(causeStackStart + 1).split('\n');
1201+
const { len, offset } = identicalSequenceRange(frames, causeFrames);
1202+
if (len > 0) {
1203+
frames.splice(offset + 1, len - 2,
1204+
ctx.stylize(` ... ${len - 2} lines matching cause stack trace ...`, 'undefined'));
11781205
}
11791206
}
11801207
}
1208+
return frames;
1209+
}
11811210

1211+
function improveStack(stack, constructor, name, tag) {
11821212
// A stack trace may contain arbitrary data. Only manipulate the output
11831213
// for "regular errors" (errors that "look normal") for now.
1214+
let len = name.length;
1215+
11841216
if (constructor === null ||
11851217
(name.endsWith('Error') &&
11861218
stack.startsWith(name) &&
@@ -1206,6 +1238,33 @@ function formatError(err, constructor, tag, ctx, keys) {
12061238
}
12071239
}
12081240
}
1241+
return stack;
1242+
}
1243+
1244+
function removeDuplicateErrorKeys(ctx, keys, err, stack) {
1245+
if (!ctx.showHidden && keys.length !== 0) {
1246+
for (const name of ['name', 'message', 'stack']) {
1247+
const index = keys.indexOf(name);
1248+
// Only hide the property in case it's part of the original stack
1249+
if (index !== -1 && stack.includes(err[name])) {
1250+
keys.splice(index, 1);
1251+
}
1252+
}
1253+
}
1254+
}
1255+
1256+
function formatError(err, constructor, tag, ctx, keys) {
1257+
const name = err.name != null ? String(err.name) : 'Error';
1258+
let stack = getStackString(err);
1259+
1260+
removeDuplicateErrorKeys(ctx, keys, err, stack);
1261+
1262+
if (err.cause && (keys.length === 0 || !keys.includes('cause'))) {
1263+
keys.push('cause')
1264+
}
1265+
1266+
stack = improveStack(stack, constructor, name, tag);
1267+
12091268
// Ignore the error message if it's contained in the stack.
12101269
let pos = (err.message && stack.indexOf(err.message)) || -1;
12111270
if (pos !== -1)
@@ -1217,7 +1276,7 @@ function formatError(err, constructor, tag, ctx, keys) {
12171276
} else if (ctx.colors) {
12181277
// Highlight userland code and node modules.
12191278
let newStack = stack.slice(0, stackStart);
1220-
const lines = stack.slice(stackStart + 1).split('\n');
1279+
const lines = getStackFrames(ctx, err, stack.slice(stackStart + 1));
12211280
for (const line of lines) {
12221281
const core = line.match(coreModuleRegExp);
12231282
if (core !== null && NativeModule.exists(core[1])) {
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
class FoobarError extends Error {
6+
status = 'Feeling good';
7+
}
8+
9+
const cause1 = new TypeError('Inner error');
10+
const cause2 = new FoobarError('Individual message', { cause: cause1 });
11+
cause2.extraProperties = 'Yes!';
12+
const cause3 = new Error('Stack causes', { cause: cause2 });
13+
14+
process.nextTick(() => {
15+
const error = new RangeError('New Stack Frames', { cause: cause2 });
16+
const error2 = new RangeError('New Stack Frames', { cause: cause3 });
17+
18+
console.log(error);
19+
console.log(cause3);
20+
console.log(error2);
21+
});
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
RangeError: New Stack Frames
2+
at *
3+
at * {
4+
[cause]: FoobarError: Individual message
5+
at *
6+
at *
7+
... 4 lines matching cause stack trace ...
8+
at * {
9+
status: 'Feeling good',
10+
extraProperties: 'Yes!',
11+
[cause]: TypeError: Inner error
12+
at *
13+
at *
14+
at *
15+
at *
16+
at *
17+
at *
18+
at *
19+
}
20+
}
21+
Error: Stack causes
22+
at *
23+
at *
24+
... 4 lines matching cause stack trace ...
25+
at * {
26+
[cause]: FoobarError: Individual message
27+
at *
28+
at *
29+
... 4 lines matching cause stack trace ...
30+
at *
31+
status: 'Feeling good',
32+
extraProperties: 'Yes!',
33+
[cause]: TypeError: Inner error
34+
at *
35+
at *
36+
at *
37+
at *
38+
at *
39+
at *
40+
at *
41+
}
42+
}
43+
RangeError: New Stack Frames
44+
at *
45+
at * {
46+
[cause]: Error: Stack causes
47+
at *
48+
at *
49+
... 4 lines matching cause stack trace ...
50+
at * {
51+
[cause]: FoobarError: Individual message
52+
at *
53+
at *
54+
... 4 lines matching cause stack trace ...
55+
at * {
56+
status: 'Feeling good',
57+
extraProperties: 'Yes!',
58+
[cause]: TypeError: Inner error
59+
at *
60+
at *
61+
at *
62+
at *
63+
at *
64+
at *
65+
at *
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)