mirror of
https://github.com/oven-sh/bun
synced 2026-02-18 14:51:52 +00:00
fix(Error): captureStackTrace with non-stack function returns proper stack string (#27017)
### What does this PR do? When Error.captureStackTrace(e, fn) is called with a function that isn't in the call stack, all frames are filtered out and e.stack should return just the error name and message (e.g. "Error: test"), matching Node.js behavior. Previously Bun returned undefined because: 1. The empty frame vector replaced the original stack frames via setStackFrames(), but the lazy stack accessor was only installed when hasMaterializedErrorInfo() was true (i.e. stack was previously accessed). When it wasn't, JSC's internal materialization saw the empty/null frames and produced no stack property at all. 2. The custom lazy getter returned undefined when stackTrace was nullptr, instead of computing the error name+message string with zero frames. Fix: always force materialization before replacing frames, always install the custom lazy accessor, and handle nullptr stackTrace in the getter by computing the error string with an empty frame list. ### How did you verify your code works? --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -641,13 +641,16 @@ JSC_DEFINE_CUSTOM_GETTER(errorInstanceLazyStackCustomGetter, (JSGlobalObject * g
|
||||
OrdinalNumber column;
|
||||
String sourceURL;
|
||||
auto stackTrace = errorObject->stackTrace();
|
||||
if (stackTrace == nullptr) {
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSValue result = computeErrorInfoToJSValue(vm, *stackTrace, line, column, sourceURL, errorObject, nullptr);
|
||||
stackTrace->clear();
|
||||
errorObject->setStackFrames(vm, {});
|
||||
JSValue result;
|
||||
if (stackTrace == nullptr) {
|
||||
WTF::Vector<JSC::StackFrame> emptyTrace;
|
||||
result = computeErrorInfoToJSValue(vm, emptyTrace, line, column, sourceURL, errorObject, nullptr);
|
||||
} else {
|
||||
result = computeErrorInfoToJSValue(vm, *stackTrace, line, column, sourceURL, errorObject, nullptr);
|
||||
stackTrace->clear();
|
||||
errorObject->setStackFrames(vm, {});
|
||||
}
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
errorObject->putDirect(vm, vm.propertyNames->stack, result, 0);
|
||||
return JSValue::encode(result);
|
||||
@@ -687,17 +690,27 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
|
||||
JSCStackTrace::getFramesForCaller(vm, callFrame, errorObject, caller, stackTrace, stackTraceLimit);
|
||||
|
||||
if (auto* instance = jsDynamicCast<JSC::ErrorInstance*>(errorObject)) {
|
||||
// Force materialization before replacing the stack frames, so that JSC's
|
||||
// internal lazy error info mechanism doesn't later see the replaced (possibly empty)
|
||||
// stack trace and fail to create the stack property.
|
||||
if (!instance->hasMaterializedErrorInfo())
|
||||
instance->materializeErrorInfoIfNeeded(vm);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
instance->setStackFrames(vm, WTF::move(stackTrace));
|
||||
if (instance->hasMaterializedErrorInfo()) {
|
||||
|
||||
{
|
||||
const auto& propertyName = vm.propertyNames->stack;
|
||||
VM::DeletePropertyModeScope scope(vm, VM::DeletePropertyMode::IgnoreConfigurable);
|
||||
VM::DeletePropertyModeScope deleteScope(vm, VM::DeletePropertyMode::IgnoreConfigurable);
|
||||
DeletePropertySlot slot;
|
||||
JSObject::deleteProperty(instance, globalObject, propertyName, slot);
|
||||
if (auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(globalObject)) {
|
||||
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, zigGlobalObject->m_lazyStackCustomGetterSetter.get(zigGlobalObject), JSC::PropertyAttribute::CustomAccessor | 0);
|
||||
} else {
|
||||
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, CustomGetterSetter::create(vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter), JSC::PropertyAttribute::CustomAccessor | 0);
|
||||
}
|
||||
}
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(globalObject)) {
|
||||
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, zigGlobalObject->m_lazyStackCustomGetterSetter.get(zigGlobalObject), JSC::PropertyAttribute::CustomAccessor | 0);
|
||||
} else {
|
||||
instance->putDirectCustomAccessor(vm, vm.propertyNames->stack, CustomGetterSetter::create(vm, errorInstanceLazyStackCustomGetter, errorInstanceLazyStackCustomSetter), JSC::PropertyAttribute::CustomAccessor | 0);
|
||||
}
|
||||
} else {
|
||||
OrdinalNumber line;
|
||||
|
||||
@@ -378,10 +378,15 @@ class AssertionError extends Error {
|
||||
this.operator = operator;
|
||||
}
|
||||
ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction);
|
||||
// JSC::Interpreter::getStackTrace() sometimes short-circuits without creating a .stack property.
|
||||
// e.g.: https://github.com/oven-sh/WebKit/blob/e32c6356625cfacebff0c61d182f759abf6f508a/Source/JavaScriptCore/interpreter/Interpreter.cpp#L501
|
||||
if ($isUndefinedOrNull(this.stack)) {
|
||||
ErrorCaptureStackTrace(this, AssertionError);
|
||||
// When all stack frames are above the stackStartFn (e.g. in async
|
||||
// contexts), captureStackTrace produces a stack with just the error
|
||||
// message and no frame lines. Retry with AssertionError as the filter
|
||||
// so we get at least the frames below the constructor.
|
||||
{
|
||||
const s = this.stack;
|
||||
if ($isUndefinedOrNull(s) || (typeof s === "string" && s.indexOf("\n at ") === -1)) {
|
||||
ErrorCaptureStackTrace(this, AssertionError);
|
||||
}
|
||||
}
|
||||
// Create error message including the error code in the name.
|
||||
this.stack; // eslint-disable-line no-unused-expressions
|
||||
|
||||
@@ -754,3 +754,39 @@ test("CallFrame.p.isAsync", async () => {
|
||||
|
||||
expect(prepare).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("captureStackTrace with constructor function not in stack returns error string", () => {
|
||||
// When the second argument to captureStackTrace is a function that isn't in
|
||||
// the call stack, all frames are filtered out and .stack should still return
|
||||
// the error name and message (matching Node.js behavior).
|
||||
function notInStack() {}
|
||||
|
||||
// Case 1: stack not accessed before captureStackTrace
|
||||
{
|
||||
const e = new Error("test");
|
||||
Error.captureStackTrace(e, notInStack);
|
||||
expect(e.stack).toBe("Error: test");
|
||||
}
|
||||
|
||||
// Case 2: stack accessed before captureStackTrace
|
||||
{
|
||||
const e = new Error("test");
|
||||
void e.stack;
|
||||
Error.captureStackTrace(e, notInStack);
|
||||
expect(e.stack).toBe("Error: test");
|
||||
}
|
||||
|
||||
// Case 3: empty message
|
||||
{
|
||||
const e = new Error();
|
||||
Error.captureStackTrace(e, notInStack);
|
||||
expect(e.stack).toBe("Error");
|
||||
}
|
||||
|
||||
// Case 4: custom error name
|
||||
{
|
||||
const e = new TypeError("bad type");
|
||||
Error.captureStackTrace(e, notInStack);
|
||||
expect(e.stack).toBe("TypeError: bad type");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user