diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 159a8459c9d28b..f8ff88e05fa7aa 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -285,11 +285,16 @@ static JSValue formatStackTraceToJSValue(JSC::VM& vm, Zig::GlobalObject* globalO size_t framesCount = callSites->length(); WTF::StringBuilder sb; + if (JSC::JSValue errorMessage = errorObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->message)) { + RETURN_IF_EXCEPTION(scope, {}); auto* str = errorMessage.toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); if (str->length() > 0) { + auto value = str->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); sb.append("Error: "_s); - sb.append(str->value(lexicalGlobalObject).data); + sb.append(value.data); } else { sb.append("Error"_s); } @@ -297,18 +302,23 @@ static JSValue formatStackTraceToJSValue(JSC::VM& vm, Zig::GlobalObject* globalO sb.append("Error"_s); } - if (framesCount > 0) { - sb.append("\n"_s); - } - for (size_t i = 0; i < framesCount; i++) { + sb.append("\n at "_s); + JSC::JSValue callSiteValue = callSites->getIndex(lexicalGlobalObject, i); - CallSite* callSite = JSC::jsDynamicCast(callSiteValue); - sb.append(" at "_s); - callSite->formatAsString(vm, lexicalGlobalObject, sb); RETURN_IF_EXCEPTION(scope, {}); - if (i != framesCount - 1) { - sb.append("\n"_s); + + if (CallSite* callSite = JSC::jsDynamicCast(callSiteValue)) { + callSite->formatAsString(vm, lexicalGlobalObject, sb); + RETURN_IF_EXCEPTION(scope, {}); + } else { + // This matches Node.js / V8's behavior + // It can become "at [object Object]" if the object is not a CallSite + auto* str = callSiteValue.toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto value = str->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + sb.append(value.data); } } @@ -684,8 +694,8 @@ static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObj for (size_t i = 0; i < framesCount; i++) { JSC::JSValue callSiteValue = callSites.at(i); - CallSite* callSite = JSC::jsDynamicCast(callSiteValue); if (remappedFrames[i].remapped) { + CallSite* callSite = JSC::jsCast(callSiteValue); callSite->setColumnNumber(remappedFrames[i].position.column()); callSite->setLineNumber(remappedFrames[i].position.line()); } diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js index 9feaa8d12a040a..69dcf9307fa928 100644 --- a/test/js/node/v8/capture-stack-trace.test.js +++ b/test/js/node/v8/capture-stack-trace.test.js @@ -663,3 +663,37 @@ test("calling .stack on a non-materialized Error updates the stack properly", fu expect(stack).toContain("hey"); expect(stack).toContain("wrapped"); }); + +test("Error.prepareStackTrace on an array with non-CallSite objects doesn't crash", () => { + const result = Error.prepareStackTrace(new Error("ok"), [{ a: 1 }, { b: 2 }, { c: 3 }]); + expect(result).toBe("Error: ok\n at [object Object]\n at [object Object]\n at [object Object]"); +}); + +test("Error.prepareStackTrace calls toString()", () => { + const result = Error.prepareStackTrace(new Error("ok"), [ + { a: 1 }, + { b: 2 }, + { + c: 3, + toString() { + return "potato"; + }, + }, + ]); + expect(result).toBe("Error: ok\n at [object Object]\n at [object Object]\n at potato"); +}); + +test("Error.prepareStackTrace propagates exceptions", () => { + expect(() => + Error.prepareStackTrace(new Error("ok"), [ + { a: 1 }, + { b: 2 }, + { + c: 3, + toString() { + throw new Error("hi"); + }, + }, + ]), + ).toThrow("hi"); +});