Skip to content

Commit ded6df5

Browse files
BridgeARBethGriggs
authored andcommitted
util: add subclass and null prototype support for errors in inspect
This adds support to visualize the difference between errors with null prototype or subclassed errors. This has a couple safeguards to be sure that the output is not intrusive. PR-URL: #26923 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent 4fe5148 commit ded6df5

File tree

2 files changed

+111
-22
lines changed

2 files changed

+111
-22
lines changed

lib/internal/util/inspect.js

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -666,25 +666,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
666666
return ctx.stylize(base, 'date');
667667
}
668668
} else if (isError(value)) {
669-
// Make error with message first say the error.
670-
base = formatError(value);
671-
// Wrap the error in brackets in case it has no stack trace.
672-
const stackStart = base.indexOf('\n at');
673-
if (stackStart === -1) {
674-
base = `[${base}]`;
675-
}
676-
// The message and the stack have to be indented as well!
677-
if (ctx.indentationLvl !== 0) {
678-
const indentation = ' '.repeat(ctx.indentationLvl);
679-
base = formatError(value).replace(/\n/g, `\n${indentation}`);
680-
}
669+
base = formatError(value, constructor, tag, ctx);
681670
if (keys.length === 0)
682671
return base;
683-
684-
if (ctx.compact === false && stackStart !== -1) {
685-
braces[0] += `${base.slice(stackStart)}`;
686-
base = `[${base.slice(0, stackStart)}]`;
687-
}
688672
} else if (isAnyArrayBuffer(value)) {
689673
// Fast path for ArrayBuffer and SharedArrayBuffer.
690674
// Can't do the same for DataView because it has a non-primitive
@@ -844,6 +828,52 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
844828
return res;
845829
}
846830

831+
function formatError(err, constructor, tag, ctx) {
832+
// TODO(BridgeAR): Always show the error code if present.
833+
let stack = err.stack || errorToString(err);
834+
835+
// A stack trace may contain arbitrary data. Only manipulate the output
836+
// for "regular errors" (errors that "look normal") for now.
837+
const name = err.name || 'Error';
838+
let len = name.length;
839+
if (constructor === null ||
840+
name.endsWith('Error') &&
841+
stack.startsWith(name) &&
842+
(stack.length === len || stack[len] === ':' || stack[len] === '\n')) {
843+
let fallback = 'Error';
844+
if (constructor === null) {
845+
const start = stack.match(/^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n {4}at)/) ||
846+
stack.match(/^([a-z_A-Z0-9-]*Error)$/);
847+
fallback = start && start[1] || '';
848+
len = fallback.length;
849+
fallback = fallback || 'Error';
850+
}
851+
const prefix = getPrefix(constructor, tag, fallback).slice(0, -1);
852+
if (name !== prefix) {
853+
if (prefix.includes(name)) {
854+
if (len === 0) {
855+
stack = `${prefix}: ${stack}`;
856+
} else {
857+
stack = `${prefix}${stack.slice(len)}`;
858+
}
859+
} else {
860+
stack = `${prefix} [${name}]${stack.slice(len)}`;
861+
}
862+
}
863+
}
864+
// Wrap the error in brackets in case it has no stack trace.
865+
const stackStart = stack.indexOf('\n at');
866+
if (stackStart === -1) {
867+
stack = `[${stack}]`;
868+
}
869+
// The message and the stack have to be indented as well!
870+
if (ctx.indentationLvl !== 0) {
871+
const indentation = ' '.repeat(ctx.indentationLvl);
872+
stack = stack.replace(/\n/g, `\n${indentation}`);
873+
}
874+
return stack;
875+
}
876+
847877
function groupArrayElements(ctx, output) {
848878
let totalLength = 0;
849879
let maxLength = 0;
@@ -991,11 +1021,6 @@ function formatPrimitive(fn, value, ctx) {
9911021
return fn(value.toString(), 'symbol');
9921022
}
9931023

994-
function formatError(value) {
995-
// TODO(BridgeAR): Always show the error code if present.
996-
return value.stack || errorToString(value);
997-
}
998-
9991024
function formatNamespaceObject(ctx, value, recurseTimes, keys) {
10001025
const output = new Array(keys.length);
10011026
for (var i = 0; i < keys.length; i++) {

test/parallel/test-util-inspect.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,70 @@ assert.strictEqual(util.inspect('"\''), '`"\'`');
16631663
// eslint-disable-next-line no-template-curly-in-string
16641664
assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
16651665

1666+
// Errors should visualize as much information as possible.
1667+
// If the name is not included in the stack, visualize it as well.
1668+
[
1669+
[class Foo extends TypeError {}, 'test'],
1670+
[class Foo extends TypeError {}, undefined],
1671+
[class BarError extends Error {}, 'test'],
1672+
[class BazError extends Error {
1673+
get name() {
1674+
return 'BazError';
1675+
}
1676+
}, undefined]
1677+
].forEach(([Class, message, messages], i) => {
1678+
console.log('Test %i', i);
1679+
const foo = new Class(message);
1680+
const name = foo.name;
1681+
const extra = Class.name.includes('Error') ? '' : ` [${foo.name}]`;
1682+
assert(
1683+
util.inspect(foo).startsWith(
1684+
`${Class.name}${extra}${message ? `: ${message}` : '\n'}`),
1685+
util.inspect(foo)
1686+
);
1687+
Object.defineProperty(foo, Symbol.toStringTag, {
1688+
value: 'WOW',
1689+
writable: true,
1690+
configurable: true
1691+
});
1692+
const stack = foo.stack;
1693+
foo.stack = 'This is a stack';
1694+
assert.strictEqual(
1695+
util.inspect(foo),
1696+
'[This is a stack]'
1697+
);
1698+
foo.stack = stack;
1699+
assert(
1700+
util.inspect(foo).startsWith(
1701+
`${Class.name} [WOW]${extra}${message ? `: ${message}` : '\n'}`),
1702+
util.inspect(foo)
1703+
);
1704+
Object.setPrototypeOf(foo, null);
1705+
assert(
1706+
util.inspect(foo).startsWith(
1707+
`[${name}: null prototype] [WOW]${message ? `: ${message}` : '\n'}`
1708+
),
1709+
util.inspect(foo)
1710+
);
1711+
foo.bar = true;
1712+
delete foo[Symbol.toStringTag];
1713+
assert(
1714+
util.inspect(foo).startsWith(
1715+
`{ [${name}: null prototype]${message ? `: ${message}` : '\n'}`),
1716+
util.inspect(foo)
1717+
);
1718+
foo.stack = 'This is a stack';
1719+
assert.strictEqual(
1720+
util.inspect(foo),
1721+
'{ [[Error: null prototype]: This is a stack] bar: true }'
1722+
);
1723+
foo.stack = stack.split('\n')[0];
1724+
assert.strictEqual(
1725+
util.inspect(foo),
1726+
`{ [[${name}: null prototype]${message ? `: ${message}` : ''}] bar: true }`
1727+
);
1728+
});
1729+
16661730
// Verify that throwing in valueOf and toString still produces nice results.
16671731
[
16681732
[new String(55), "[String: '55']"],

0 commit comments

Comments
 (0)