Skip to content

Commit da87413

Browse files
BridgeARdanielleadams
authored andcommitted
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]> PR-URL: #41002 Fixes: #40859 Fixes: #38725 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent a959f4f commit da87413

File tree

3 files changed

+261
-30
lines changed

3 files changed

+261
-30
lines changed

lib/internal/util/inspect.js

+94-30
Original file line numberDiff line numberDiff line change
@@ -1162,25 +1162,58 @@ 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 = MathMin(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+
const skipped = len - 2;
1204+
const msg = ` ... ${skipped} lines matching cause stack trace ...`;
1205+
frames.splice(offset + 1, skipped, ctx.stylize(msg, 'undefined'));
11781206
}
11791207
}
11801208
}
1209+
return frames;
1210+
}
11811211

1212+
function improveStack(stack, constructor, name, tag) {
11821213
// A stack trace may contain arbitrary data. Only manipulate the output
11831214
// for "regular errors" (errors that "look normal") for now.
1215+
let len = name.length;
1216+
11841217
if (constructor === null ||
11851218
(name.endsWith('Error') &&
11861219
stack.startsWith(name) &&
@@ -1206,6 +1239,33 @@ function formatError(err, constructor, tag, ctx, keys) {
12061239
}
12071240
}
12081241
}
1242+
return stack;
1243+
}
1244+
1245+
function removeDuplicateErrorKeys(ctx, keys, err, stack) {
1246+
if (!ctx.showHidden && keys.length !== 0) {
1247+
for (const name of ['name', 'message', 'stack']) {
1248+
const index = keys.indexOf(name);
1249+
// Only hide the property in case it's part of the original stack
1250+
if (index !== -1 && stack.includes(err[name])) {
1251+
keys.splice(index, 1);
1252+
}
1253+
}
1254+
}
1255+
}
1256+
1257+
function formatError(err, constructor, tag, ctx, keys) {
1258+
const name = err.name != null ? String(err.name) : 'Error';
1259+
let stack = getStackString(err);
1260+
1261+
removeDuplicateErrorKeys(ctx, keys, err, stack);
1262+
1263+
if (err.cause && (keys.length === 0 || !keys.includes('cause'))) {
1264+
keys.push('cause');
1265+
}
1266+
1267+
stack = improveStack(stack, constructor, name, tag);
1268+
12091269
// Ignore the error message if it's contained in the stack.
12101270
let pos = (err.message && stack.indexOf(err.message)) || -1;
12111271
if (pos !== -1)
@@ -1214,27 +1274,31 @@ function formatError(err, constructor, tag, ctx, keys) {
12141274
const stackStart = stack.indexOf('\n at', pos);
12151275
if (stackStart === -1) {
12161276
stack = `[${stack}]`;
1217-
} else if (ctx.colors) {
1218-
// Highlight userland code and node modules.
1277+
} else {
12191278
let newStack = stack.slice(0, stackStart);
1220-
const lines = stack.slice(stackStart + 1).split('\n');
1221-
for (const line of lines) {
1222-
const core = line.match(coreModuleRegExp);
1223-
if (core !== null && NativeModule.exists(core[1])) {
1224-
newStack += `\n${ctx.stylize(line, 'undefined')}`;
1225-
} else {
1226-
// This adds underscores to all node_modules to quickly identify them.
1227-
let nodeModule;
1228-
newStack += '\n';
1229-
let pos = 0;
1230-
while (nodeModule = nodeModulesRegExp.exec(line)) {
1231-
// '/node_modules/'.length === 14
1232-
newStack += line.slice(pos, nodeModule.index + 14);
1233-
newStack += ctx.stylize(nodeModule[1], 'module');
1234-
pos = nodeModule.index + nodeModule[0].length;
1279+
const lines = getStackFrames(ctx, err, stack.slice(stackStart + 1));
1280+
if (ctx.colors) {
1281+
// Highlight userland code and node modules.
1282+
for (const line of lines) {
1283+
const core = line.match(coreModuleRegExp);
1284+
if (core !== null && NativeModule.exists(core[1])) {
1285+
newStack += `\n${ctx.stylize(line, 'undefined')}`;
1286+
} else {
1287+
// This adds underscores to all node_modules to quickly identify them.
1288+
let nodeModule;
1289+
newStack += '\n';
1290+
let pos = 0;
1291+
while (nodeModule = nodeModulesRegExp.exec(line)) {
1292+
// '/node_modules/'.length === 14
1293+
newStack += line.slice(pos, nodeModule.index + 14);
1294+
newStack += ctx.stylize(nodeModule[1], 'module');
1295+
pos = nodeModule.index + nodeModule[0].length;
1296+
}
1297+
newStack += pos === 0 ? line : line.slice(pos);
12351298
}
1236-
newStack += pos === 0 ? line : line.slice(pos);
12371299
}
1300+
} else {
1301+
newStack += `\n${lines.join('\n')}`;
12381302
}
12391303
stack = newStack;
12401304
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const { inspect } = require('util');
6+
7+
class FoobarError extends Error {
8+
status = 'Feeling good';
9+
}
10+
11+
const cause1 = new TypeError('Inner error');
12+
const cause2 = new FoobarError('Individual message', { cause: cause1 });
13+
cause2.extraProperties = 'Yes!';
14+
const cause3 = new Error('Stack causes', { cause: cause2 });
15+
16+
process.nextTick(() => {
17+
const error = new RangeError('New Stack Frames', { cause: cause2 });
18+
const error2 = new RangeError('New Stack Frames', { cause: cause3 });
19+
20+
inspect.defaultOptions.colors = true;
21+
22+
console.log(inspect(error));
23+
console.log(inspect(cause3));
24+
console.log(inspect(error2));
25+
26+
inspect.defaultOptions.colors = false;
27+
28+
console.log(inspect(error));
29+
console.log(inspect(cause3));
30+
console.log(inspect(error2));
31+
});
+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
RangeError: New Stack Frames
2+
at *
3+
*[90m at *[39m {
4+
[cause]: FoobarError: Individual message
5+
at *
6+
*[90m at *[39m
7+
*[90m ... 4 lines matching cause stack trace ...*[39m
8+
*[90m at *[39m {
9+
status: *[32m'Feeling good'*[39m,
10+
extraProperties: *[32m'Yes!'*[39m,
11+
[cause]: TypeError: Inner error
12+
at *
13+
*[90m at *[39m
14+
*[90m at *[39m
15+
*[90m at *[39m
16+
*[90m at *[39m
17+
*[90m at *[39m
18+
*[90m at *[39m
19+
}
20+
}
21+
Error: Stack causes
22+
at *
23+
*[90m at *[39m
24+
*[90m ... 4 lines matching cause stack trace ...*[39m
25+
*[90m at *[39m {
26+
[cause]: FoobarError: Individual message
27+
at *
28+
*[90m at *[39m
29+
*[90m ... 4 lines matching cause stack trace ...*[39m
30+
*[90m at *[39m {
31+
status: *[32m'Feeling good'*[39m,
32+
extraProperties: *[32m'Yes!'*[39m,
33+
[cause]: TypeError: Inner error
34+
at *
35+
*[90m at *[39m
36+
*[90m at *[39m
37+
*[90m at *[39m
38+
*[90m at *[39m
39+
*[90m at *[39m
40+
*[90m at *[39m
41+
}
42+
}
43+
RangeError: New Stack Frames
44+
at *
45+
*[90m at *[39m {
46+
[cause]: Error: Stack causes
47+
at *
48+
*[90m at *[39m
49+
*[90m ... 4 lines matching cause stack trace ...*[39m
50+
*[90m at *[39m {
51+
[cause]: FoobarError: Individual message
52+
at *
53+
*[90m at *[39m
54+
*[90m ... 4 lines matching cause stack trace ...*[39m
55+
*[90m at *[39m {
56+
status: *[32m'Feeling good'*[39m,
57+
extraProperties: *[32m'Yes!'*[39m,
58+
[cause]: TypeError: Inner error
59+
at *
60+
*[90m at *[39m
61+
*[90m at *[39m
62+
*[90m at *[39m
63+
*[90m at *[39m
64+
*[90m at *[39m
65+
*[90m at *[39m
66+
}
67+
}
68+
}
69+
RangeError: New Stack Frames
70+
at *
71+
at * {
72+
[cause]: FoobarError: Individual message
73+
at *
74+
at *
75+
... 4 lines matching cause stack trace ...
76+
at * {
77+
status: 'Feeling good',
78+
extraProperties: 'Yes!',
79+
[cause]: TypeError: Inner error
80+
at *
81+
at *
82+
at *
83+
at *
84+
at *
85+
at *
86+
at *
87+
}
88+
}
89+
Error: Stack causes
90+
at *
91+
at *
92+
... 4 lines matching cause stack trace ...
93+
at * {
94+
[cause]: FoobarError: Individual message
95+
at *
96+
at *
97+
... 4 lines matching cause stack trace ...
98+
at *
99+
status: 'Feeling good',
100+
extraProperties: 'Yes!',
101+
[cause]: TypeError: Inner error
102+
at *
103+
at *
104+
at *
105+
at *
106+
at *
107+
at *
108+
at *
109+
}
110+
}
111+
RangeError: New Stack Frames
112+
at *
113+
at * {
114+
[cause]: Error: Stack causes
115+
at *
116+
at *
117+
... 4 lines matching cause stack trace ...
118+
at * {
119+
[cause]: FoobarError: Individual message
120+
at *
121+
at *
122+
... 4 lines matching cause stack trace ...
123+
at * {
124+
status: 'Feeling good',
125+
extraProperties: 'Yes!',
126+
[cause]: TypeError: Inner error
127+
at *
128+
at *
129+
at *
130+
at *
131+
at *
132+
at *
133+
at *
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)