Files
scriptc/tests/corpus/6874-error-capture-stack-trace-objects.cjs
T
Chris Tate a897079be1 Capture onto the target evaluated before a constructorOpt that reassigns it
When Error.captureStackTrace evaluates a JavaScript constructorOpt (a getter,
a call, or this.constructor with a constructor getter), V8 has already
evaluated the target. The lowering kept the target in a local only when its
expression was not a plain read, so a constructorOpt that assigned the target
variable made the capture install the stack on the new value. The target is
now always kept in a local before the constructorOpt runs.
2026-10-27 13:00:48 -05:00

175 lines
5.6 KiB
JavaScript

// Error.captureStackTrace on objects in JavaScript, as Node does it: an own,
// non-enumerable, configurable `stack` accessor giving the object's heading
// (its name and message when first read, through Error.prototype.toString's
// rules, "Error" by default) and the frames from the caller; TypeErrors for a
// primitive and a non-extensible target; and a constructorOpt getter or call
// that runs after the target and before the capture.
'use strict';
function frames(stack) {
const lines = String(stack).split('\n');
const names = lines.slice(1).map((line) => line.trim().split(' ')[1]);
return `${lines[0]} | ${names.filter((name) => ['inner', 'middle', 'outer', 'skipped', 'caller'].includes(name)).join(',')}`;
}
function inner(target) {
Error.captureStackTrace(target);
return target;
}
function middle(target) {
return inner(target);
}
function outer(target) {
return middle(target);
}
console.log('plain', frames(outer({}).stack));
console.log('named', frames(outer({ name: 'Parse', message: 'bad token' }).stack));
console.log('message only', frames(outer({ message: 'just this' }).stack));
console.log('empty name', frames(outer({ name: '', message: 'no name' }).stack));
console.log('number fields', frames(outer({ name: 7, message: 8 }).stack));
const own = inner({ kept: 1 });
console.log('keys', JSON.stringify(Object.keys(own)), typeof own.stack);
const descriptor = Object.getOwnPropertyDescriptor(own, 'stack');
console.log('descriptor', typeof descriptor.get, typeof descriptor.set, descriptor.enumerable, descriptor.configurable);
own.stack = 'replaced';
console.log('writable', own.stack);
const later = inner({ message: 'first' });
later.name = 'Renamed';
later.message = 'second';
console.log('formatted when read', frames(later.stack));
later.name = 'Again';
console.log('formatted once', frames(later.stack));
const counted = { message: 'm' };
let reads = 0;
Object.defineProperty(counted, 'name', { get() { reads++; return 'Counted'; } });
inner(counted);
console.log('reads before stack', reads);
console.log('counted', frames(counted.stack), reads);
function skipped(target) {
Error.captureStackTrace(target, skipped);
return target;
}
function caller(target) {
return skipped(target);
}
console.log('constructorOpt', frames(caller({}).stack));
const arr = [];
Error.captureStackTrace(arr);
console.log('array', frames(arr.stack), arr.length);
for (const value of [5, 'text', undefined, null, true]) {
try {
Error.captureStackTrace(value);
console.log('primitive', String(value), 'no error');
} catch (error) {
console.log('primitive', String(value), error.name, error.message);
}
}
try {
Error.captureStackTrace(Object.freeze({}));
console.log('frozen', 'no error');
} catch (error) {
console.log('frozen', error.name, error.message);
}
const api = {
run(target) {
Error.captureStackTrace(target, api.run);
return target;
},
};
function viaApi(target) {
return api.run(target);
}
console.log('member constructorOpt', frames(viaApi({ message: 'member' }).stack).split(' | ')[0]);
const captureFn = Error.captureStackTrace;
function viaValue(target) {
captureFn(target);
return target;
}
const fromValue = viaValue({ message: 'value' });
fromValue.message = 'read later';
console.log('function value', frames(fromValue.stack), captureFn.name, captureFn.length);
try {
captureFn(7);
} catch (error) {
console.log('function value primitive', error.name, error.message);
}
const order = [];
const holder = {
get ctor() {
order.push('getter');
return inner;
},
};
function pick() {
order.push('call');
return inner;
}
function note(label, value) {
order.push(label);
return value;
}
Error.captureStackTrace(note('target', {}), holder.ctor);
Error.captureStackTrace(note('target', {}), pick());
console.log('constructorOpt evaluated', order.join(','));
const throwing = {
get ctor() {
throw new RangeError('from constructorOpt');
},
};
const untouched = {};
try {
Error.captureStackTrace(untouched, throwing.ctor);
console.log('throwing constructorOpt', 'no error');
} 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);
}
// The stack goes on the target as evaluated before constructorOpt, even when
// constructorOpt assigns the variable or field the target was read from.
let target = { message: 'original' };
const replacement = { message: 'replacement' };
const swapping = {
get ctor() {
target = replacement;
return inner;
},
};
const original = target;
Error.captureStackTrace(target, swapping.ctor);
console.log('reassigned variable', 'stack' in original, 'stack' in replacement, target === replacement);
const box = { error: { message: 'boxed' } };
const boxed = box.error;
function swapField() {
box.error = { message: 'swapped in' };
return inner;
}
Error.captureStackTrace(box.error, swapField());
console.log('reassigned field', 'stack' in boxed, 'stack' in box.error);