Skip to content

runtime: honor cause options for Error subclasses and AggregateError #2836

Closed

Description

@andrewtdiz

Summary

Perry partially supports new Error(message, { cause }), but the lowering only recognizes a literal object option on the base Error constructor. Node applies the ES2022 cause option across native Error subclasses and AggregateError, and it reads the cause from ordinary runtime option objects, not only AST object literals.

This leaves TypeError, RangeError, ReferenceError, SyntaxError, AggregateError, and new Error(message, optionsVariable) without the expected .cause value.

Node behavior

A local Node v25.9.0 probe shows:

const cause = { why: 1 };
const opts = { cause };
console.log(new Error("outer", opts).cause === cause);        // true
console.log(new RangeError("range", { cause: "c" }).cause);  // "c"
console.log(new SyntaxError("syntax", { cause: 123 }).cause); // 123

const agg = new AggregateError([1, 2], "many", { cause });
console.log(agg.name, agg.message, agg.errors, agg.cause === cause);
// AggregateError many [1, 2] true

The .cause property is non-enumerable, but it is directly readable.

Current Perry evidence

  • crates/perry-runtime/src/error.rs::ErrorHeader already has a cause slot, and js_error_new_with_cause(message, cause) stores it for base Error objects.
  • crates/perry-hir/src/lower/expr_new.rs only enters the two-argument cause-detection branch when class_name == "Error".
  • That branch only recognizes { cause: ... } or shorthand { cause } at the AST object-literal level. If the options object is held in a variable or produced dynamically, lowering evaluates it for side effects and emits plain Expr::ErrorNew with no cause.
  • The same file lowers TypeError, RangeError, ReferenceError, and SyntaxError by taking only the first argument and emitting Expr::TypeErrorNew / RangeErrorNew / ReferenceErrorNew / SyntaxErrorNew, so their second options argument is ignored.
  • AggregateError lowering reads only the first two constructor args (errors, message) into Expr::AggregateErrorNew { errors, message }; there is no options/cause field in that IR variant and crates/perry-runtime/src/error.rs::js_aggregateerror_new() only sets .errors.
  • PR Inspect Error cause details like Node #2151 improved util.inspect display for existing error causes, but it does not make constructors populate .cause for these missing forms.

Suggested test surface

Add node-suite tests for:

const cause = { why: 1 };
const opts = { cause };
console.log("error dynamic:", new Error("outer", opts).cause === cause);
console.log("type cause:", new TypeError("bad", { cause }).cause === cause);
console.log("range cause:", new RangeError("bad", { cause: "r" }).cause);
console.log("syntax cause:", new SyntaxError("bad", { cause: 123 }).cause);

const agg = new AggregateError([1, 2], "many", { cause });
console.log("agg cause:", agg.cause === cause, JSON.stringify(agg.errors));

Expected output should show each constructor preserving the cause option.

Scope / non-goals

This issue is specifically about constructor option handling for .cause. It does not cover stack-frame formatting / source mapping (#1278), or AggregateError's broader iterable validation semantics.

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