Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/jsc/bindings/AsyncStackTrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "BunClientData.h"
#include "ErrorStackFrame.h"
#include "FormatStackTraceForJS.h"

#include <JavaScriptCore/CodeBlock.h>
#include <JavaScriptCore/ErrorInstance.h>
Expand Down Expand Up @@ -162,17 +163,20 @@ extern "C" void Bun__attachAsyncStackFromPromise(JSC::JSGlobalObject* globalObje
// would desync m_stackTrace from the cached property.
if (instance->hasMaterializedErrorInfo())
return;
if (auto* existing = instance->stackTrace(); existing && !existing->isEmpty())
// Null: no capture at all (Error.stackTraceLimit deleted) or a stack string is already set (structuredClone, GC finalizer).
auto* existing = instance->stackTrace();
if (!existing || !existing->isEmpty())
return;

size_t limit = globalObject->stackTraceLimit().value_or(10);
if (!limit)
return;

WTF::Vector<JSC::StackFrame> frames;
collectAsyncStackFramesFromPromise(vm, instance, promise, frames, limit);
if (frames.isEmpty())
return;
size_t limit = globalObject->stackTraceLimit().value_or(Bun::DEFAULT_ERROR_STACK_TRACE_LIMIT);
if (limit) {
WTF::Vector<JSC::StackFrame> frames;
collectAsyncStackFramesFromPromise(vm, instance, promise, frames, limit);
if (!frames.isEmpty()) {
instance->setStackFrames(vm, WTF::move(frames));
return;
}
}

instance->setStackFrames(vm, WTF::move(frames));
Bun::installLazyStackIfFrameless(vm, globalObject, instance);
}
2 changes: 2 additions & 0 deletions src/jsc/bindings/ErrorCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <openssl/err.h>
#include "ErrorCode.h"
#include "ErrorStackTrace.h"
#include "FormatStackTraceForJS.h"
#include "KeyObject.h"

namespace WTF {
Expand Down Expand Up @@ -216,6 +217,7 @@ JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, E
// exception were thrown by ErrorInstance::create)
return uncheckedDowncast<JSObject>(thrown_exception->value());
}
Bun::installLazyStackIfFrameless(vm, globalObject, created_error);
return created_error;
}

Expand Down
22 changes: 22 additions & 0 deletions src/jsc/bindings/FormatStackTraceForJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,28 @@ JSC_DEFINE_CUSTOM_SETTER(errorInstanceLazyStackCustomSetter, (JSGlobalObject * g
return true;
}

void installLazyStackIfFrameless(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::ErrorInstance* error)
{
auto* stackTrace = error->stackTrace();
// Null means Error.stackTraceLimit was deleted; V8 leaves .stack undefined for that too.
if (!stackTrace || !stackTrace->isEmpty())
return;

// Already installed, or assigned explicitly.
if (error->getDirect(vm, vm.propertyNames->stack))
return;

auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
error->putDirectCustomAccessor(vm, vm.propertyNames->stack, globalObject->m_lazyStackCustomGetterSetter.get(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::CustomAccessor | 0);
}

JSC::JSValue installLazyStackIfFrameless(JSC::JSGlobalObject* globalObject, JSC::JSValue value)
{
if (auto* error = dynamicDowncast<JSC::ErrorInstance>(value))
installLazyStackIfFrameless(JSC::getVM(globalObject), globalObject, error);
return value;
}

JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
Zig::GlobalObject* globalObject = static_cast<Zig::GlobalObject*>(lexicalGlobalObject);
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/FormatStackTraceForJS.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ JSC_DECLARE_HOST_FUNCTION(jsFunctionDefaultErrorPrepareStackTrace);
JSC_DECLARE_CUSTOM_GETTER(errorInstanceLazyStackCustomGetter);
JSC_DECLARE_CUSTOM_SETTER(errorInstanceLazyStackCustomSetter);

// JSC defines no .stack at all for an error that captured zero frames; V8 still gives it "Name: message".
void installLazyStackIfFrameless(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::ErrorInstance* error);
// Same, for constructors that hand back a JSValue; anything that is not an ErrorInstance passes through.
JSC::JSValue installLazyStackIfFrameless(JSC::JSGlobalObject* globalObject, JSC::JSValue value);

// Internal wrapper functions for JSC error info callbacks
WTF::String computeErrorInfoWrapperToString(JSC::VM& vm, WTF::Vector<JSC::StackFrame>& stackTrace, unsigned int& line_in, unsigned int& column_in, WTF::String& sourceURL, void* bunErrorData);
JSC::JSValue computeErrorInfoWrapperToJSValue(JSC::VM& vm, WTF::Vector<JSC::StackFrame>& stackTrace, unsigned int& line_in, unsigned int& column_in, WTF::String& sourceURL, JSC::JSObject* errorInstance, void* bunErrorData);
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/S3Error.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <wtf/Compiler.h>
#include "ZigGeneratedClasses.h"
#include "S3Error.h"
#include "FormatStackTraceForJS.h"

namespace Bun {

Expand Down Expand Up @@ -38,6 +39,7 @@ SYSV_ABI JSC::EncodedJSValue S3Error__toErrorInstance(const S3Error* arg0,

auto prototype = defaultGlobalObject(globalObject)->m_S3ErrorStructure.getInitializedOnMainThread(globalObject);
JSC::JSObject* result = JSC::ErrorInstance::create(vm, prototype, message, {});
installLazyStackIfFrameless(globalObject, result);
result->putDirect(vm, vm.propertyNames->name, defaultGlobalObject(globalObject)->commonStrings().s3ErrorString(globalObject), JSC::PropertyAttribute::DontEnum | 0);
if (err.code.tag != BunStringTag::Empty) {
JSC::JSValue code = Bun::toJS(globalObject, err.code);
Expand Down
27 changes: 17 additions & 10 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
#include "ErrorStackFrame.h"
#include "AsyncStackTrace.h"
#include "ErrorStackTrace.h"
#include "FormatStackTraceForJS.h"
#include "ObjectBindings.h"

#include <JavaScriptCore/VMInlines.h>
Expand Down Expand Up @@ -2583,6 +2584,7 @@ static JSC::EncodedJSValue systemErrorToErrorInstance(const SystemError* arg0, J
auto& names = WebCore::builtinNames(vm);

JSC::JSObject* result = createError(globalObject, errorType, message);
Bun::installLazyStackIfFrameless(globalObject, result);

auto clientData = WebCore::clientData(vm);

Expand Down Expand Up @@ -2664,6 +2666,7 @@ JSC::EncodedJSValue SystemError__toErrorInstanceWithInfoObject(const SystemError
auto message = makeString("A system error occurred: "_s, syscallString, " returned "_s, codeString, " ("_s, messageString, ")"_s);

JSC::JSObject* result = JSC::ErrorInstance::create(vm, JSC::ErrorInstance::createStructure(vm, globalObject, globalObject->errorPrototype()), message, {});
Bun::installLazyStackIfFrameless(globalObject, result);
JSC::JSObject* info = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 0);

auto clientData = WebCore::clientData(vm);
Expand Down Expand Up @@ -3631,14 +3634,18 @@ JSC::EncodedJSValue JSC__JSGlobalObject__createAggregateError(JSC::JSGlobalObjec

JSC::Structure* errorStructure = globalObject->errorStructure(JSC::ErrorType::AggregateError);

RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::createAggregateError(vm, errorStructure, array, message, cause, nullptr, JSC::TypeNothing, false)));
auto* error = JSC::createAggregateError(vm, errorStructure, array, message, cause, nullptr, JSC::TypeNothing, false);
Bun::installLazyStackIfFrameless(vm, globalObject, error);
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(error));
}
JSC::EncodedJSValue JSC__JSGlobalObject__createAggregateErrorWithArray(JSC::JSGlobalObject* global, JSC::JSArray* array, BunString message, JSValue cause)
{
auto& vm = JSC::getVM(global);
JSC::Structure* errorStructure = global->errorStructure(JSC::ErrorType::AggregateError);
WTF::String messageString = message.toWTFString();
return JSC::JSValue::encode(JSC::createAggregateError(vm, errorStructure, array, messageString, cause, nullptr, JSC::TypeNothing, false));
auto* error = JSC::createAggregateError(vm, errorStructure, array, messageString, cause, nullptr, JSC::TypeNothing, false);
Bun::installLazyStackIfFrameless(vm, global, error);
return JSC::JSValue::encode(error);
}

JSC::EncodedJSValue ZigString__toAtomicValue(const ZigString* arg0, JSC::JSGlobalObject* arg1)
Expand Down Expand Up @@ -3757,27 +3764,27 @@ JSC::EncodedJSValue ZigString__toExternalValueWithCallback(const ZigString* arg0

JSC::EncodedJSValue ZigString__toErrorInstance(const ZigString* str, JSC::JSGlobalObject* globalObject)
{
return JSC::JSValue::encode(Zig::getErrorInstance(str, globalObject));
return JSC::JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, Zig::getErrorInstance(str, globalObject)));
}

JSC::EncodedJSValue ZigString__toTypeErrorInstance(const ZigString* str, JSC::JSGlobalObject* globalObject)
{
return JSC::JSValue::encode(Zig::getTypeErrorInstance(str, globalObject));
return JSC::JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, Zig::getTypeErrorInstance(str, globalObject)));
}

JSC::EncodedJSValue ZigString__toDOMExceptionInstance(const ZigString* str, JSC::JSGlobalObject* globalObject, WebCore::ExceptionCode code)
{
return JSValue::encode(createDOMException(globalObject, code, toStringCopy(*str)));
return JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, createDOMException(globalObject, code, toStringCopy(*str))));
}

JSC::EncodedJSValue ZigString__toSyntaxErrorInstance(const ZigString* str, JSC::JSGlobalObject* globalObject)
{
return JSC::JSValue::encode(Zig::getSyntaxErrorInstance(str, globalObject));
return JSC::JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, Zig::getSyntaxErrorInstance(str, globalObject)));
}

JSC::EncodedJSValue ZigString__toRangeErrorInstance(const ZigString* str, JSC::JSGlobalObject* globalObject)
{
return JSC::JSValue::encode(Zig::getRangeErrorInstance(str, globalObject));
return JSC::JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, Zig::getRangeErrorInstance(str, globalObject)));
}

JSC::JSPromise*
Expand Down Expand Up @@ -6335,17 +6342,17 @@ CPP_DECL void JSC__VM__performOpportunisticallyScheduledTasks(JSC::VM* vm, doubl

extern "C" EncodedJSValue JSC__createError(JSC::JSGlobalObject* globalObject, const BunString* str)
{
return JSValue::encode(JSC::createError(globalObject, str->toWTFString(BunString::ZeroCopy)));
return JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, JSC::createError(globalObject, str->toWTFString(BunString::ZeroCopy))));
}

extern "C" EncodedJSValue JSC__createTypeError(JSC::JSGlobalObject* globalObject, const BunString* str)
{
return JSValue::encode(JSC::createTypeError(globalObject, str->toWTFString(BunString::ZeroCopy)));
return JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, JSC::createTypeError(globalObject, str->toWTFString(BunString::ZeroCopy))));
}

extern "C" EncodedJSValue JSC__createRangeError(JSC::JSGlobalObject* globalObject, const BunString* str)
{
return JSValue::encode(JSC::createRangeError(globalObject, str->toWTFString(BunString::ZeroCopy)));
return JSValue::encode(Bun::installLazyStackIfFrameless(globalObject, JSC::createRangeError(globalObject, str->toWTFString(BunString::ZeroCopy))));
}

extern "C" EncodedJSValue ExpectMatcherUtils__getSingleton(JSC::JSGlobalObject* globalObject_)
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/node/crypto/CryptoUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <JavaScriptCore/ArrayBuffer.h>
#include "CryptoKeyRaw.h"
#include "JSKeyObject.h"
#include "FormatStackTraceForJS.h"

namespace Bun {

Expand Down Expand Up @@ -382,6 +383,7 @@ JSValue createCryptoError(JSC::JSGlobalObject* globalObject, ThrowScope& scope,
// Create error object with the message
JSC::JSObject* errorObject = createError(globalObject, errorMessage);
RETURN_IF_EXCEPTION(scope, {});
installLazyStackIfFrameless(globalObject, errorObject);

PutPropertySlot messageSlot(errorObject, false);
errorObject->put(errorObject, globalObject, Identifier::fromString(vm, "message"_s), jsString(vm, errorMessage), messageSlot);
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/webcore/WebSocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
#include "JSBuffer.h"
#include "BunClientData.h"
#include "ErrorEvent.h"
#include "FormatStackTraceForJS.h"
#include "WebSocketDeflate.h"

namespace WebCore {
Expand All @@ -76,7 +77,7 @@ static ErrorEvent::Init createErrorEventInit(WebSocket& webSocket, const String&
eventInit.bubbles = false;
eventInit.cancelable = false;
eventInit.colno = 0;
eventInit.error = JSC::createError(globalObject, eventInit.message);
eventInit.error = Bun::installLazyStackIfFrameless(globalObject, JSC::createError(globalObject, eventInit.message));
return eventInit;
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/api/js_bundle_completion_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ impl JSBundleCompletionTask {
global_this,
BunString::static_(b"Bundle failed"),
);
return promise.reject(global_this, aggregate_error);
return promise.reject_with_async_stack(global_this, aggregate_error);
} else {
return promise.resolve(global_this, build_result);
}
Expand Down
17 changes: 17 additions & 0 deletions test/js/node/crypto/crypto-sign-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,20 @@ test("crypto.sign() supports all RSA digest variants", () => {
expect(signature.length).toBeGreaterThan(0);
}
});

test("an error from the sign job is passed to the callback with a .stack", async () => {
// A PSS salt longer than the modulus makes the signing itself fail, so the error is
// created when the thread pool job completes, with no JS on the stack (as in node).
const { promise, resolve } = Promise.withResolvers<Error | null>();
sign(
"sha256",
Buffer.from("test message"),
{ key: DUMMY_PRIVATE_KEY, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: 4000 },
resolve,
);
const err = (await promise)!;

expect(err).toBeInstanceOf(Error);
expect(Object.prototype.hasOwnProperty.call(err, "stack")).toBe(true);
expect(err.stack).toBe(`Error: ${err.message}`);
});
24 changes: 18 additions & 6 deletions test/js/node/fs/promises.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ test("fs.promises async stack through Promise subclass", async () => {

expect(caught).toBeDefined();
expect(caught.code).toBe("ENOENT");
// Subclass .then() may not preserve the reaction chain — must not crash.
expect(typeof caught.stack === "string" || caught.stack === undefined).toBe(true);
// Subclass .then() may not preserve the reaction chain; the error still gets a .stack.
expect(caught.stack).toStartWith(`Error: ${caught.message}`);
});

test("fs.promises async stack through custom thenable", async () => {
Expand All @@ -282,8 +282,8 @@ test("fs.promises async stack through custom thenable", async () => {

expect(caught).toBeDefined();
expect(caught.code).toBe("ENOENT");
// Custom thenables break the direct reaction chain — must not crash.
expect(typeof caught.stack === "string" || caught.stack === undefined).toBe(true);
// Custom thenables break the direct reaction chain; the error still gets a .stack.
expect(caught.stack).toStartWith(`Error: ${caught.message}`);
});

test("fs.promises async stack with Promise.all", async () => {
Expand All @@ -300,8 +300,20 @@ test("fs.promises async stack with Promise.all", async () => {

expect(caught).toBeDefined();
expect(caught.code).toBe("ENOENT");
// Promise.all uses combinator context — must not crash.
expect(typeof caught.stack === "string" || caught.stack === undefined).toBe(true);
// Promise.all uses combinator context, so no async frames are recovered; the error
// still gets a .stack.
expect(caught.stack).toStartWith(`Error: ${caught.message}`);
});

test("fs.promises errors that nothing awaits still have a .stack", async () => {
// .catch() attaches a handler but nothing awaits the derived promise, so there is no
// async chain to recover frames from: the error keeps an empty trace.
const caught = await new Promise(resolve => readFile("/nonexistent-path/x.txt").catch(resolve));

expect(caught.code).toBe("ENOENT");
expect(caught.stack).toBe(`Error: ${caught.message}`);
expect(Object.prototype.hasOwnProperty.call(caught, "stack")).toBe(true);
expect(Object.keys(caught)).not.toContain("stack");
});

it("an unused FileHandle.writer() does not prevent close()", async () => {
Expand Down
Loading
Loading