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
- Args dropped: When the implicit ctor doesn't crash,
.name and .message come back undefined — meaning the "hello" arg never reached Error().
- 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:
-
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.
-
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
Repro
bun main.ts:perry main.ts(exits 133 = SIGABRT):If you give the subclass an explicit
constructor(msg) { super(msg); }, both.nameand.messagework correctly. The issue is the implicit default constructor of a bare-derivedextends Error.Per ECMAScript spec,
class MyError extends Error {}is shorthand forclass MyError extends Error { constructor(...args) { super(...args); } }. Args should pass through toError()and produce a real Error with.messageset.Two symptoms, same root cause
.nameand.messagecome back undefined — meaning the"hello"arg never reachedError().e.nameaccess crashes withexit 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-clientvia #551. The package's error hierarchy is:Every error thrown by s3-lite arrives at the catch site with
e.message === undefinedande.name === undefinedbecause 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:
Implicit-ctor arg forwarding: when a class declaration has no explicit constructor and
extends X, generate the equivalent ofconstructor(...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 throughError.Error initialization integrity: ensure
Error.prototype.constructor(msg)actually populatesthis.messageandthis.namewhen called from a derived class'ssuper(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:class A extends Error {}; class B extends A {}; new B("x").message === "x"class A extends Error {}; class B extends A { constructor(msg) { super(msg); } }e instanceof Error/e instanceof MyErrorboth truee.stackis a string (or at least not crashing)String(e)returns"Error: hello"per Error.prototype.toString specRefs #551