Skip to content

Error subclass name is an own enumerable property: JSON.stringify(err) is {"name":"Error"} vs node {}, and getOwnPropertyNames omits stack #9440

Description

@proggeramlug
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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions