Skip to content

Commit 0f615d4

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> Signed-off-by: Beth Griggs <Bethany.Griggs@uk.ibm.com>
1 parent 17cc117 commit 0f615d4

File tree

2 files changed

+111
-21
lines changed

2 files changed

+111
-21
lines changed

lib/internal/util/inspect.js

+47-21
Original file line numberDiff line numberDiff line change
@@ -643,25 +643,9 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
643643
return ctx.stylize(base, 'date');
644644
}
645645
} else if (isError(value)) {
646-
// Make error with message first say the error.
647-
base = formatError(value);
648-
// Wrap the error in brackets in case it has no stack trace.
649-
const stackStart = base.indexOf('\n at');
650-
if (stackStart === -1) {
651-
base = `[${base}]`;
652-
}
653-
// The message and the stack have to be indented as well!
654-
if (ctx.indentationLvl !== 0) {
655-
const indentation = ' '.repeat(ctx.indentationLvl);
656-
base = formatError(value).replace(/\n/g, `\n${indentation}`);
657-
}
646+
base = formatError(value, constructor, tag, ctx);
658647
if (keys.length === 0)
659648
return base;
660-
661-
if (ctx.compact === false && stackStart !== -1) {
662-
braces[0] += `${base.slice(stackStart)}`;
663-
base = `[${base.slice(0, stackStart)}]`;
664-
}
665649
} else if (isAnyArrayBuffer(value)) {
666650
// Fast path for ArrayBuffer and SharedArrayBuffer.
667651
// Can't do the same for DataView because it has a non-primitive
@@ -821,6 +805,52 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
821805
return res;
822806
}
823807

808+
function formatError(err, constructor, tag, ctx) {
809+
// TODO(BridgeAR): Always show the error code if present.
810+
let stack = err.stack || errorToString(err);
811+
812+
// A stack trace may contain arbitrary data. Only manipulate the output
813+
// for "regular errors" (errors that "look normal") for now.
814+
const name = err.name || 'Error';
815+
let len = name.length;
816+
if (constructor === null ||
817+
name.endsWith('Error') &&
818+
stack.startsWith(name) &&
819+
(stack.length === len || stack[len] === ':' || stack[len] === '\n')) {
820+
let fallback = 'Error';
821+
if (constructor === null) {
822+
const start = stack.match(/^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n {4}at)/) ||
823+
stack.match(/^([a-z_A-Z0-9-]*Error)$/);
824+
fallback = start && start[1] || '';
825+
len = fallback.length;
826+
fallback = fallback || 'Error';
827+
}
828+
const prefix = getPrefix(constructor, tag, fallback).slice(0, -1);
829+
if (name !== prefix) {
830+
if (prefix.includes(name)) {
831+
if (len === 0) {
832+
stack = `${prefix}: ${stack}`;
833+
} else {
834+
stack = `${prefix}${stack.slice(len)}`;
835+
}
836+
} else {
837+
stack = `${prefix} [${name}]${stack.slice(len)}`;
838+
}
839+
}
840+
}
841+
// Wrap the error in brackets in case it has no stack trace.
842+
const stackStart = stack.indexOf('\n at');
843+
if (stackStart === -1) {
844+
stack = `[${stack}]`;
845+
}
846+
// The message and the stack have to be indented as well!
847+
if (ctx.indentationLvl !== 0) {
848+
const indentation = ' '.repeat(ctx.indentationLvl);
849+
stack = stack.replace(/\n/g, `\n${indentation}`);
850+
}
851+
return stack;
852+
}
853+
824854
function groupArrayElements(ctx, output) {
825855
let totalLength = 0;
826856
let maxLength = 0;
@@ -968,10 +998,6 @@ function formatPrimitive(fn, value, ctx) {
968998
return fn(value.toString(), 'symbol');
969999
}
9701000

971-
function formatError(value) {
972-
return value.stack || errorToString(value);
973-
}
974-
9751001
function formatNamespaceObject(ctx, value, recurseTimes, keys) {
9761002
const output = new Array(keys.length);
9771003
for (var i = 0; i < keys.length; i++) {

test/parallel/test-util-inspect.js

+64
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,70 @@ assert.strictEqual(util.inspect('"\''), '`"\'`');
16431643
// eslint-disable-next-line no-template-curly-in-string
16441644
assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
16451645

1646+
// Errors should visualize as much information as possible.
1647+
// If the name is not included in the stack, visualize it as well.
1648+
[
1649+
[class Foo extends TypeError {}, 'test'],
1650+
[class Foo extends TypeError {}, undefined],
1651+
[class BarError extends Error {}, 'test'],
1652+
[class BazError extends Error {
1653+
get name() {
1654+
return 'BazError';
1655+
}
1656+
}, undefined]
1657+
].forEach(([Class, message, messages], i) => {
1658+
console.log('Test %i', i);
1659+
const foo = new Class(message);
1660+
const name = foo.name;
1661+
const extra = Class.name.includes('Error') ? '' : ` [${foo.name}]`;
1662+
assert(
1663+
util.inspect(foo).startsWith(
1664+
`${Class.name}${extra}${message ? `: ${message}` : '\n'}`),
1665+
util.inspect(foo)
1666+
);
1667+
Object.defineProperty(foo, Symbol.toStringTag, {
1668+
value: 'WOW',
1669+
writable: true,
1670+
configurable: true
1671+
});
1672+
const stack = foo.stack;
1673+
foo.stack = 'This is a stack';
1674+
assert.strictEqual(
1675+
util.inspect(foo),
1676+
'[This is a stack]'
1677+
);
1678+
foo.stack = stack;
1679+
assert(
1680+
util.inspect(foo).startsWith(
1681+
`${Class.name} [WOW]${extra}${message ? `: ${message}` : '\n'}`),
1682+
util.inspect(foo)
1683+
);
1684+
Object.setPrototypeOf(foo, null);
1685+
assert(
1686+
util.inspect(foo).startsWith(
1687+
`[${name}: null prototype] [WOW]${message ? `: ${message}` : '\n'}`
1688+
),
1689+
util.inspect(foo)
1690+
);
1691+
foo.bar = true;
1692+
delete foo[Symbol.toStringTag];
1693+
assert(
1694+
util.inspect(foo).startsWith(
1695+
`{ [${name}: null prototype]${message ? `: ${message}` : '\n'}`),
1696+
util.inspect(foo)
1697+
);
1698+
foo.stack = 'This is a stack';
1699+
assert.strictEqual(
1700+
util.inspect(foo),
1701+
'{ [[Error: null prototype]: This is a stack] bar: true }'
1702+
);
1703+
foo.stack = stack.split('\n')[0];
1704+
assert.strictEqual(
1705+
util.inspect(foo),
1706+
`{ [[${name}: null prototype]${message ? `: ${message}` : ''}] bar: true }`
1707+
);
1708+
});
1709+
16461710
// Verify that throwing in valueOf and toString still produces nice results.
16471711
[
16481712
[new String(55), "[String: '55']"],

0 commit comments

Comments
 (0)