class MyErr extends Error {}
const e = new MyErr("boom");
JSON.stringify(e) // node {} perry {"name":"Error"}
Object.getOwnPropertyNames(e) // node ["message","stack"] perry ["message","name"]
Two divergences from one cause. Errors carry a dedicated ErrorHeader with a name field (crates/perry-runtime/src/error.rs) that the own-key walk surfaces as own and enumerable. Node keeps name on Error.prototype, so it is not an own property at all and never appears in JSON.stringify or getOwnPropertyNames.
The stack half is separate and already addressed in #9432, which installs an own non-enumerable stack accessor. name is untouched by that PR — one of its comments even records the current state as "enumerability, different reflection — a deliberate simplification". This issue is to retire that simplification.
Why it matters beyond reflection
Serializing an error is routine in logging, IPC and session transcripts, so the wrong shape travels: a consumer that does JSON.stringify(err) gets a name key node never emits, and anything reconstructing an error from getOwnPropertyNames misses stack.
It was checked as a candidate cause of #9421 (cc transcripts truncated) and ruled out — it changes serialized content, not record count — but it is a real defect on its own.
Fix shape
name should live on the prototype like node's, with the ErrorHeader field kept as the storage behind it rather than exposed as an own key. Anything that special-cases name in the own-key walk (js_object_keys, getOwnPropertyNames, the JSON own-key path) needs to stop reporting it.
Watch the subclass case specifically: class MyErr extends Error {} produces a GC_TYPE_OBJECT rather than an ErrorHeader (see #9410), so the two paths must agree after the change. extends_builtin_error(class_id) is the existing disambiguator.
Verification bar
A gap fixture byte-compared to node --experimental-strip-types, demonstrated failing on a compiler built from unfixed origin/main, covering: Error itself and a subclass; JSON.stringify; Object.getOwnPropertyNames; Object.keys; a for…in loop; spread into an object literal; an explicitly assigned err.name = "Custom" (which must become an own enumerable property, as in node); and util.inspect output.
Found by the differential stress-test of claude-code under perry, while falsifying #9421's async-flush premise.
Two divergences from one cause. Errors carry a dedicated
ErrorHeaderwith anamefield (crates/perry-runtime/src/error.rs) that the own-key walk surfaces as own and enumerable. Node keepsnameonError.prototype, so it is not an own property at all and never appears inJSON.stringifyorgetOwnPropertyNames.The
stackhalf is separate and already addressed in #9432, which installs an own non-enumerablestackaccessor.nameis untouched by that PR — one of its comments even records the current state as "enumerability, different reflection — a deliberate simplification". This issue is to retire that simplification.Why it matters beyond reflection
Serializing an error is routine in logging, IPC and session transcripts, so the wrong shape travels: a consumer that does
JSON.stringify(err)gets anamekey node never emits, and anything reconstructing an error fromgetOwnPropertyNamesmissesstack.It was checked as a candidate cause of #9421 (cc transcripts truncated) and ruled out — it changes serialized content, not record count — but it is a real defect on its own.
Fix shape
nameshould live on the prototype like node's, with theErrorHeaderfield kept as the storage behind it rather than exposed as an own key. Anything that special-casesnamein the own-key walk (js_object_keys,getOwnPropertyNames, the JSON own-key path) needs to stop reporting it.Watch the subclass case specifically:
class MyErr extends Error {}produces aGC_TYPE_OBJECTrather than anErrorHeader(see #9410), so the two paths must agree after the change.extends_builtin_error(class_id)is the existing disambiguator.Verification bar
A gap fixture byte-compared to
node --experimental-strip-types, demonstrated failing on a compiler built from unfixedorigin/main, covering:Erroritself and a subclass;JSON.stringify;Object.getOwnPropertyNames;Object.keys; afor…inloop; spread into an object literal; an explicitly assignederr.name = "Custom"(which must become an own enumerable property, as in node); andutil.inspectoutput.Found by the differential stress-test of claude-code under perry, while falsifying #9421's async-flush premise.