From c90b321a5b03e33a08c87df1fd64871b39865608 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 13:59:33 -0500 Subject: [PATCH] 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' } }`. --- packages/runtime/src/scr_inspect.c | 30 ++++++++++++++++++++++ tests/corpus/6525-error-subclass-fields.ts | 9 +++++-- tests/corpus/6530-error-subclass-header.ts | 20 ++++++++++++--- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 0b307e572..223517e92 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -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 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); diff --git a/tests/corpus/6525-error-subclass-fields.ts b/tests/corpus/6525-error-subclass-fields.ts index 7f44df19c..06164baf8 100644 --- a/tests/corpus/6525-error-subclass-fields.ts +++ b/tests/corpus/6525-error-subclass-fields.ts @@ -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))); diff --git a/tests/corpus/6530-error-subclass-header.ts b/tests/corpus/6530-error-subclass-header.ts index cd261ae73..9a33d5eb4 100644 --- a/tests/corpus/6530-error-subclass-header.ts +++ b/tests/corpus/6530-error-subclass-header.ts @@ -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 }));