Print an error's non-enumerable cause and an AggregateError's errors in its checked-dynamic rendering

An error rendered through its checked-dynamic node (a runtime error class,
or an error held as a subclass's `cause`) prints a non-enumerable `cause`
as `[cause]: value` and an AggregateError's `errors` as `[errors]: [...]`,
after its enumerable properties, as Node's formatError does. Before, a
subclass's cause that was an Error constructed with a `cause` option
printed without it: `{ cause: [Error: inner] }` where Node prints
`{ cause: [Error: inner] { [cause]: 'deep' } }`.
This commit is contained in:
Chris Tate
2026-10-20 13:59:33 -05:00
parent e0d14fefaa
commit c90b321a5b
3 changed files with 54 additions and 5 deletions
+30
View File
@@ -1070,6 +1070,16 @@ static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) {
* scr_insp_error: the stackless bracket form `[Name: message]`, then the
* remaining enumerable own properties (code, errno, syscall, ...) in
* Node's `{ ... }` tail. */
/* An own non-enumerable entry of the error encoding, NULL when absent or
* enumerable (an enumerable one prints with the other own keys). */
static const ScrDynEntry *insp_error_hidden_entry(const ScrDyn *d, const char *key, size_t len) {
for (size_t i = 0; i < d->v.obj.len; i++) {
const ScrDynEntry *ent = &d->v.obj.entries[i];
if (ent->key_len == len && memcmp(ent->key, key, len) == 0) return ent->enumerable ? NULL : ent;
}
return NULL;
}
static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
/* An error already on the traversal stack (a spawnSync error's own
* `error`): Node's [Circular *N], and <ref *N> on the outer rendering. */
@@ -1090,6 +1100,12 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
continue;
extra++;
}
/* formatError shows a non-enumerable `cause`, and an AggregateError's
* `errors` array, after the enumerable keys: `[cause]`, `[errors]`. */
const ScrDynEntry *cause = insp_error_hidden_entry(d, "cause", 5);
const ScrDynEntry *errors = insp_error_hidden_entry(d, "errors", 6);
if (errors && errors->value->kind != SCR_DYN_ARR) errors = NULL;
extra += (cause != NULL) + (errors != NULL);
InspBuf base = {0};
ib_char(&base, '[');
if (name != NULL) ib_str(&base, name->v.str);
@@ -1132,6 +1148,20 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
scr_insp_entry(entry, false);
scr_str_release(entry);
}
const ScrDynEntry *hidden[] = {cause, errors};
for (size_t i = 0; i < 2; i++) {
if (!hidden[i]) continue;
ScrStr *val = scr_insp_dyn(hidden[i]->value, recurse + 1, value_depth);
InspBuf eb = {0};
ib_char(&eb, '[');
ib_bytes(&eb, hidden[i]->key, hidden[i]->key_len);
ib_cstr(&eb, "]: ");
ib_bytes(&eb, val->data, val->len);
scr_str_release(val);
ScrStr *entry = ib_take(&eb);
scr_insp_entry(entry, false);
scr_str_release(entry);
}
ScrStr *b0 = scr_str_new("{", 1);
ScrStr *b1 = scr_str_new("}", 1);
ScrStr *body = scr_insp_end(base_str, b0, b1, recurse + 1, false, false);
+7 -2
View File
@@ -3,8 +3,8 @@
// (`[Bailable [Error]: m]`), followed by the own properties — on one line
// or broken across lines by the same width rule as any object, and as
// `[Name]` beyond the depth budget. A `cause` follows the fields: `cause:`
// when assigned, `[cause]:` from the constructor's options. (The stack is
// cleared: compiled errors carry none.)
// when assigned, `[cause]:` from the constructor's options, and a cause
// prints its own cause. (The stack is cleared: compiled errors carry none.)
import * as util from "node:util";
class Bailable extends Error {
bail = false;
@@ -48,3 +48,8 @@ show(clean(new Retried("with an undefined cause")));
const assigned = clean(new Bailable("assigned"));
assigned.cause = { code: 1 };
show(assigned);
const root = new Error("root");
root.stack = "";
const middle: Error = new TypeError("middle", { cause: root });
middle.stack = "";
show(clean(new Retried("nested causes", middle)));
+17 -3
View File
@@ -3,9 +3,10 @@
// `[FetchFailedError: m]` when the constructor name already contains the
// error's name; runtime error classes keep their own header. Own `cause`
// properties (declared, assigned in the constructor or later) and a
// declared `name` follow Node's rendering, and beyond the depth limit an
// instance with own properties renders as `[Constructor]`. (The stack is
// cleared: compiled errors carry none.)
// declared `name` follow Node's rendering, as does a cause's own
// `[cause]` and an AggregateError cause's `[errors]`, and beyond the depth
// limit an instance with own properties renders as `[Constructor]`. (The
// stack is cleared: compiled errors carry none.)
import * as util from "node:util";
class Plain extends Error {}
@@ -64,3 +65,16 @@ const multiline = clean(new Plain("first\nsecond"));
console.log(util.inspect({ multiline }));
const deep = [clean(new Assigned("beyond depth")), clean(new Assigned()), clean(new Plain())];
for (const error of deep) console.log(util.inspect({ a: { b: error } }, { depth: 1 }));
const inner = new Error("inner", { cause: "deep" });
inner.stack = "";
console.log(util.inspect(inner));
const wrapped = clean(new Plain("outer"));
wrapped.cause = inner;
console.log(util.inspect(wrapped));
const first = new Error("first");
first.stack = "";
const aggregate: Error = new AggregateError([first], "all failed", { cause: "why" });
aggregate.stack = "";
const holder = clean(new Plain("holder"));
holder.cause = aggregate;
console.log(util.inspect(holder, { depth: 4 }));