From 6c63d8cd10ca8e099bc93eb3665a34e54bf16055 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 26 Oct 2026 21:39:37 -0500 Subject: [PATCH] Run a constructor getter that this.constructor reads in Error.captureStackTrace Inside a class constructor, Error.captureStackTrace(this, this.constructor) now reads this.constructor before the capture when the class or a base class declares a constructor getter, as V8 evaluates the argument: the getter runs and its exception propagates. --- .../src/frontend/lowering/builtins/errors.ts | 16 ++++++++++ .../corpus/6873-error-capture-stack-trace.ts | 29 +++++++++++++++++++ ...6874-error-capture-stack-trace-objects.cjs | 29 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 1eb2686fd..7e2bb6d37 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -431,6 +431,16 @@ function enclosingConstructorClass(node: ts.Node): ts.ClassLikeDeclaration | und return undefined; } +/** Whether `access` (`this.constructor`) reads a `constructor` accessor + * declared on the class or a base class. */ +function readsConstructorAccessor(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + const property = lowerer.checker.getPropertyOfType( + lowerer.typeOf(access.expression), + "constructor", + ); + return property !== undefined && (property.flags & ts.SymbolFlags.GetAccessor) !== 0; +} + /** `Error.captureStackTrace(target, constructorOpt?)`, also through `?.`: * target.stack becomes the target's heading and the current stack frames, * as V8 does it (scr_error_capture_stack_trace). The target is an error, a @@ -504,6 +514,12 @@ export function lowerErrorCaptureStackTraceCall( const sig = resolved !== undefined ? lowerer.fnSigsBySymbol.get(resolved) : undefined; if (constructing && cls !== undefined) { skip = false; + // `this.constructor` runs a `constructor` getter of the class, whose + // calls and exceptions are observable. + if (ts.isPropertyAccessExpression(node) && readsConstructorAccessor(lowerer, node)) { + const value = lowerer.lowerExpr(ctor); + if (!isSafeToDiscard(value)) evaluated = value; + } } else if (sig !== undefined) { lowerer.noteEdge(sig.name); through = sig.name; diff --git a/tests/corpus/6873-error-capture-stack-trace.ts b/tests/corpus/6873-error-capture-stack-trace.ts index 62db5572d..922588870 100644 --- a/tests/corpus/6873-error-capture-stack-trace.ts +++ b/tests/corpus/6873-error-capture-stack-trace.ts @@ -85,3 +85,32 @@ console.log("limit", frames(outer(new Error("limited")).stack)); Error.stackTraceLimit = 0; console.log("limit 0", JSON.stringify(outer(new Error("none")).stack)); Error.stackTraceLimit = 10; + +// `this.constructor` in a constructor runs a `constructor` getter, and its +// exception propagates. +class Watched extends Error { + constructor() { + super("watched"); + Error.captureStackTrace(this, this.constructor); + } + get ["constructor"](): typeof Watched { + console.log("constructor getter ran"); + return Watched; + } +} +console.log("getter", typeof new Watched().stack); +class Throwing extends Error { + constructor() { + super("throwing"); + Error.captureStackTrace(this, this.constructor); + } + get ["constructor"](): typeof Throwing { + throw new RangeError("constructor getter threw"); + } +} +try { + new Throwing(); + console.log("getter did not throw"); +} catch (e: any) { + console.log("getter threw", e instanceof RangeError, e.message); +} diff --git a/tests/corpus/6874-error-capture-stack-trace-objects.cjs b/tests/corpus/6874-error-capture-stack-trace-objects.cjs index 94a47680c..456874b4e 100644 --- a/tests/corpus/6874-error-capture-stack-trace-objects.cjs +++ b/tests/corpus/6874-error-capture-stack-trace-objects.cjs @@ -121,3 +121,32 @@ try { } catch (caught) { console.log('throwing constructorOpt', caught.name, caught.message, 'stack' in untouched); } + +// `this.constructor` in a constructor runs a `constructor` getter, and its +// exception propagates. +class Watched extends Error { + constructor() { + super('watched'); + Error.captureStackTrace(this, this.constructor); + } + get ['constructor']() { + console.log('constructor getter ran'); + return Watched; + } +} +console.log('getter', typeof new Watched().stack); +class Throwing extends Error { + constructor() { + super('throwing'); + Error.captureStackTrace(this, this.constructor); + } + get ['constructor']() { + throw new RangeError('constructor getter threw'); + } +} +try { + new Throwing(); + console.log('getter did not throw'); +} catch (e) { + console.log('getter threw', e instanceof RangeError, e.message); +}