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.
This commit is contained in:
Chris Tate
2026-10-26 21:39:37 -05:00
parent 466ae48980
commit 6c63d8cd10
3 changed files with 74 additions and 0 deletions
@@ -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;
@@ -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);
}
@@ -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);
}