Skip to content

runtime: class X extends Error {} (bare) loses constructor args + SIGABRT on property access #573

Description

@proggeramlug

Repro

class MyError extends Error {}

const e = new MyError("hello");
console.log("step b: constructed");
console.log("step c: name =", e.name);   // perry: SIGABRT (or undefined, depending on layout)
console.log("step d: message =", e.message);

bun main.ts:

step b: constructed
step c: name = Error
step d: message = hello

perry main.ts (exits 133 = SIGABRT):

step b: constructed
[crash before step c]

If you give the subclass an explicit constructor(msg) { super(msg); }, both .name and .message work correctly. The issue is the implicit default constructor of a bare-derived extends Error.

Per ECMAScript spec, class MyError extends Error {} is shorthand for class MyError extends Error { constructor(...args) { super(...args); } }. Args should pass through to Error() and produce a real Error with .message set.

Two symptoms, same root cause

  1. Args dropped: When the implicit ctor doesn't crash, .name and .message come back undefined — meaning the "hello" arg never reached Error().
  2. SIGABRT on property access: With slightly different surrounding code, e.name access crashes with exit 133. Likely the implicit ctor leaves the instance in a partially-initialized state and property reads dereference garbage.

The crash is non-deterministic (different unrelated console.log calls before the access change whether it crashes or returns undefined), which suggests heap-layout sensitivity — classic uninitialized-memory pattern.

Why this matters

Discovered while compiling @bradenmacdonald/s3-lite-client via #551. The package's error hierarchy is:

export class S3Error extends Error {}
export class InvalidArgumentError extends S3Error {}
export class InvalidEndpointError extends S3Error {}
export class InvalidBucketNameError extends S3Error {
  constructor(public readonly bucketName: string) { super(`Invalid bucket name: ${bucketName}`); }
}
// ... and more

Every error thrown by s3-lite arrives at the catch site with e.message === undefined and e.name === undefined because of the bare-derived chain.

This is the canonical pattern across the entire JS error-class ecosystem — every npm package that defines its own error types uses class FooError extends Error {}. drizzle, hono, prisma, axios, every error-typed library.

Suggested implementation

Two fronts:

  1. Implicit-ctor arg forwarding: when a class declaration has no explicit constructor and extends X, generate the equivalent of constructor(...args) { super(...args); }. Verify args pass through to the immediate parent's constructor — should already work for non-Error parents per the v0.5.616 cross-module ctor-chain fix; this is the same shape but specifically through Error.

  2. Error initialization integrity: ensure Error.prototype.constructor(msg) actually populates this.message and this.name when called from a derived class's super(msg). The crash hint is that Error-typed instances may be shaped differently than regular class instances and the subclass instance layout doesn't match what the message-property accessor expects.

Acceptance

The repro at the top prints name = Error / message = hello. Plus regression covering:

  • 2-level bare chain: class A extends Error {}; class B extends A {}; new B("x").message === "x"
  • Mixed: class A extends Error {}; class B extends A { constructor(msg) { super(msg); } }
  • e instanceof Error / e instanceof MyError both true
  • e.stack is a string (or at least not crashing)
  • String(e) returns "Error: hello" per Error.prototype.toString spec

Refs #551

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