diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index c538322f8e39..f97b39f7b0f7 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -27,6 +27,7 @@ #include "napi_handle_scope.h" #include "NativePromiseContext.h" #include "StrongRootBlock.h" +#include "JSDOMException.h" namespace WebCore { using namespace JSC; @@ -39,6 +40,7 @@ JSHeapData::JSHeapData(Heap& heap) , m_heapCellTypeForBakeGlobalObject(JSC::IsoHeapCellType::Args()) , m_heapCellTypeForNapiHandleScopeImpl(JSC::IsoHeapCellType::Args()) , m_heapCellTypeForNativePromiseContext(JSC::IsoHeapCellType::Args()) + , m_heapCellTypeForJSDOMException(JSC::IsoHeapCellType::Args()) , m_domConstructorSpace ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, JSDOMConstructorBase) , m_domNamespaceObjectSpace ISO_SUBSPACE_INIT(heap, heap.cellHeapCellType, JSDOMObject) , m_subspaces(makeUnique()) diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 21a163421c03..ee40e62644d8 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -97,6 +97,7 @@ class JSHeapData { JSC::IsoHeapCellType m_heapCellTypeForNapiHandleScopeImpl; JSC::IsoHeapCellType m_heapCellTypeForBakeGlobalObject; JSC::IsoHeapCellType m_heapCellTypeForNativePromiseContext; + JSC::IsoHeapCellType m_heapCellTypeForJSDOMException; // JSC::IsoHeapCellType m_heapCellTypeForGeneratedClass; private: diff --git a/src/jsc/bindings/FormatStackTraceForJS.cpp b/src/jsc/bindings/FormatStackTraceForJS.cpp index d622679f31aa..cbd0f4d09b2a 100644 --- a/src/jsc/bindings/FormatStackTraceForJS.cpp +++ b/src/jsc/bindings/FormatStackTraceForJS.cpp @@ -22,6 +22,7 @@ #include "BunClientData.h" #include "CallSite.h" #include "ErrorStackTrace.h" +#include "JSDOMException.h" #include "headers-handwritten.h" #include @@ -414,10 +415,15 @@ static String computeErrorInfoWithoutPrepareStackTrace( if (!lexicalGlobalObject) { lexicalGlobalObject = errorInstance->globalObject(); } - name = instance->sanitizedNameString(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); - message = instance->sanitizedMessageString(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* domException = dynamicDowncast(instance)) { + name = domException->displayName(vm); + message = domException->displayMessage(vm); + } else { + name = instance->sanitizedNameString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + message = instance->sanitizedMessageString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + } } } @@ -673,8 +679,12 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncAppendStackTrace, (JSC::JSGlobalObj } if (source->stackTrace()) { - destination->stackTrace()->appendVector(*source->stackTrace()); - source->stackTrace()->clear(); + // setStackFrames takes the cellLock that JSDOMException::visitChildren reads under. + WTF::Vector combined; + combined.appendVector(*destination->stackTrace()); + combined.appendVector(*source->stackTrace()); + destination->setStackFrames(vm, WTF::move(combined)); + source->setStackFrames(vm, {}); } return JSC::JSValue::encode(jsUndefined()); @@ -724,7 +734,8 @@ JSC_DEFINE_CUSTOM_GETTER(errorInstanceLazyStackCustomGetter, (JSGlobalObject * g WTF::Vector emptyTrace; result = computeErrorInfoToJSValue(vm, emptyTrace, line, column, sourceURL, errorObject, nullptr); } else { - auto ownedStackTrace = makeUnique>(WTF::move(*stackTrace)); + // Copy: a move here races JSDOMException::visitChildren reading under the cellLock. + auto ownedStackTrace = makeUnique>(*stackTrace); JSC::MarkedArgumentBuffer protectedFrameCells; protectedFrameCells.ensureCapacity(ownedStackTrace->size() * 2); for (auto& frame : *ownedStackTrace) { diff --git a/src/jsc/bindings/JSDOMExceptionHandling.cpp b/src/jsc/bindings/JSDOMExceptionHandling.cpp index 9db6f0f283de..ca98ae7a0f27 100644 --- a/src/jsc/bindings/JSDOMExceptionHandling.cpp +++ b/src/jsc/bindings/JSDOMExceptionHandling.cpp @@ -103,7 +103,9 @@ String retrieveErrorMessage(JSGlobalObject& lexicalGlobalObject, VM& vm, JSValue // FIXME: Web Inspector: WebCore::reportException should not evaluate JavaScript handling exceptions // If this is a custom exception object, call toString on it to try and get a nice string representation for the exception. String errorMessage; - if (auto* error = dynamicDowncast(exception)) + if (auto* error = dynamicDowncast(exception)) + errorMessage = error->displayHeader(vm); + else if (auto* error = dynamicDowncast(exception)) errorMessage = error->sanitizedToString(&lexicalGlobalObject); else errorMessage = exception.toWTFString(&lexicalGlobalObject); @@ -184,7 +186,6 @@ JSValue createDOMException(JSGlobalObject* lexicalGlobalObject, ExceptionCode ec JSValue errorObject = toJS(lexicalGlobalObject, globalObject, DOMException::create(ec, message)); ASSERT(errorObject); - addErrorInfo(lexicalGlobalObject, asObject(errorObject), true); return errorObject; } } diff --git a/src/jsc/bindings/JSDOMWrapperCache.h b/src/jsc/bindings/JSDOMWrapperCache.h index 657938a92584..d6bcd1461e62 100644 --- a/src/jsc/bindings/JSDOMWrapperCache.h +++ b/src/jsc/bindings/JSDOMWrapperCache.h @@ -45,15 +45,15 @@ template JSC::JSObject* getDOMPrototype(JSC::VM&, JSDOMGl JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, JSC::ArrayBuffer*); void* wrapperKey(JSC::ArrayBuffer*); -std::optional getInlineCachedWrapper(DOMWrapperWorld&, void*); +std::optional getInlineCachedWrapper(DOMWrapperWorld&, void*); std::optional getInlineCachedWrapper(DOMWrapperWorld&, ScriptWrappable*); std::optional getInlineCachedWrapper(DOMWrapperWorld&, JSC::ArrayBuffer*); -bool setInlineCachedWrapper(DOMWrapperWorld&, void*, JSDOMObject*, JSC::WeakHandleOwner*); +bool setInlineCachedWrapper(DOMWrapperWorld&, void*, JSC::JSObject*, JSC::WeakHandleOwner*); bool setInlineCachedWrapper(DOMWrapperWorld&, ScriptWrappable*, JSDOMObject* wrapper, JSC::WeakHandleOwner* wrapperOwner); bool setInlineCachedWrapper(DOMWrapperWorld&, JSC::ArrayBuffer*, JSC::JSArrayBuffer* wrapper, JSC::WeakHandleOwner* wrapperOwner); -bool clearInlineCachedWrapper(DOMWrapperWorld&, void*, JSDOMObject*); +bool clearInlineCachedWrapper(DOMWrapperWorld&, void*, JSC::JSObject*); bool clearInlineCachedWrapper(DOMWrapperWorld&, ScriptWrappable*, JSDOMObject* wrapper); bool clearInlineCachedWrapper(DOMWrapperWorld&, JSC::ArrayBuffer*, JSC::JSArrayBuffer* wrapper); @@ -98,9 +98,9 @@ inline void* wrapperKey(JSC::ArrayBuffer* domObject) return domObject; } -inline std::optional getInlineCachedWrapper(DOMWrapperWorld&, void*) { return std::nullopt; } -inline bool setInlineCachedWrapper(DOMWrapperWorld&, void*, JSDOMObject*, JSC::WeakHandleOwner*) { return false; } -inline bool clearInlineCachedWrapper(DOMWrapperWorld&, void*, JSDOMObject*) { return false; } +inline std::optional getInlineCachedWrapper(DOMWrapperWorld&, void*) { return std::nullopt; } +inline bool setInlineCachedWrapper(DOMWrapperWorld&, void*, JSC::JSObject*, JSC::WeakHandleOwner*) { return false; } +inline bool clearInlineCachedWrapper(DOMWrapperWorld&, void*, JSC::JSObject*) { return false; } inline std::optional getInlineCachedWrapper(DOMWrapperWorld& world, ScriptWrappable* domObject) { diff --git a/src/jsc/bindings/ZigException.cpp b/src/jsc/bindings/ZigException.cpp index ea84d276eda9..511bc50b9c0e 100644 --- a/src/jsc/bindings/ZigException.cpp +++ b/src/jsc/bindings/ZigException.cpp @@ -34,6 +34,7 @@ #include "ZigGlobalObject.h" #include "helpers.h" #include "JavaScriptCore/JSObjectInlines.h" +#include "JSDOMException.h" #include "wtf/Assertions.h" #include "wtf/text/OrdinalNumber.h" @@ -456,19 +457,17 @@ static void populateStackTrace(JSC::VM& vm, const WTF::Vector& static JSC::JSValue getNonObservable(JSC::VM& vm, JSC::JSGlobalObject* global, JSC::JSObject* obj, const JSC::PropertyName& propertyName) { + auto scope = DECLARE_THROW_SCOPE(vm); PropertySlot slot = PropertySlot(obj, PropertySlot::InternalMethodType::VMInquiry, &vm); - if (obj->getNonIndexPropertySlot(global, propertyName, slot)) { - if (slot.isAccessor()) { - return {}; - } - - JSValue value = slot.getValue(global, propertyName); - if (!value || value.isUndefinedOrNull()) { - return {}; - } - return value; - } - return {}; + bool found = obj->getNonIndexPropertySlot(global, propertyName, slot); + RETURN_IF_EXCEPTION(scope, {}); + // isValue() also rejects custom getters (DOMException.prototype.code), which isAccessor() does not. + if (!found || !slot.isValue()) + return {}; + JSValue value = slot.getValue(global, propertyName); + if (!value || value.isUndefinedOrNull()) + return {}; + return value; } static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global, @@ -512,7 +511,10 @@ static void fromErrorInstance(ZigException& except, JSC::JSGlobalObject* global, return; } - except.name = Bun::toStringRef(err->sanitizedNameString(global)); + if (auto* domException = dynamicDowncast(err)) + except.name = Bun::toStringRef(domException->displayName(vm)); + else + except.name = Bun::toStringRef(err->sanitizedNameString(global)); if (!scope.clearExceptionExceptTermination()) [[unlikely]] { return; } diff --git a/src/jsc/bindings/webcore/JSDOMException.cpp b/src/jsc/bindings/webcore/JSDOMException.cpp index 6be9398307ab..a12bb4487f25 100644 --- a/src/jsc/bindings/webcore/JSDOMException.cpp +++ b/src/jsc/bindings/webcore/JSDOMException.cpp @@ -38,12 +38,15 @@ #include #include +#include #include #include +#include #include #include #include #include +#include namespace WebCore { using namespace JSC; @@ -152,19 +155,14 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSDOMExceptionDOMConstru RETURN_IF_EXCEPTION(throwScope, {}); } - auto object = DOMException::create(WTF::move(message), WTF::move(name)); - if constexpr (IsExceptionOr) - RETURN_IF_EXCEPTION(throwScope, {}); - static_assert(TypeOrExceptionOrUnderlyingType::isRef); - auto jsValue = toJSNewlyCreated>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, WTF::move(object)); - if constexpr (IsExceptionOr) - RETURN_IF_EXCEPTION(throwScope, {}); - setSubclassStructureIfNeeded(lexicalGlobalObject, callFrame, asObject(jsValue)); + // Not toJSNewlyCreated: own properties must wait until after the subclass structure swap. + auto* wrapper = createWrapper(castedThis->globalObject(), DOMException::create(WTF::move(message), WTF::move(name))); + setSubclassStructureIfNeeded(lexicalGlobalObject, callFrame, wrapper); RETURN_IF_EXCEPTION(throwScope, {}); - if (!cause.isEmpty()) { - jsValue.getObject()->putDirect(vm, vm.propertyNames->cause, cause, JSC::PropertyAttribute::DontEnum | 0); - } - return JSValue::encode(jsValue); + wrapper->putHeaderStackIfNoFrames(vm); + if (!cause.isEmpty()) + wrapper->putDirect(vm, vm.propertyNames->cause, cause, JSC::PropertyAttribute::DontEnum | 0); + return JSValue::encode(wrapper); } JSC_ANNOTATE_HOST_FUNCTION(JSDOMExceptionDOMConstructorConstruct, JSDOMExceptionDOMConstructor::construct); @@ -232,18 +230,85 @@ void JSDOMExceptionPrototype::finishCreation(VM& vm) const ClassInfo JSDOMException::s_info = { "DOMException"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDOMException) }; JSDOMException::JSDOMException(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) - : JSDOMWrapper(structure, globalObject, WTF::move(impl)) + : Base(globalObject.vm(), structure, JSC::ErrorType::Error) + , m_wrapped(WTF::move(impl)) { } void JSDOMException::finishCreation(VM& vm) { - Base::finishCreation(vm); + // Null message/cause: those stay prototype accessors over the wrapped impl. + Base::finishCreation(vm, String(), JSValue(), nullptr, JSC::TypeNothing, true); ASSERT(inherits(info())); + // No own properties here: the constructor may still swap in a subclass structure. +} + +void JSDOMException::setStackString(VM& vm, String&& stack) +{ + putDirect(vm, vm.propertyNames->stack, jsString(vm, WTF::move(stack)), static_cast(JSC::PropertyAttribute::DontEnum)); + setStackFrames(vm, {}); // Nothing will read the captured frames now; stop visitChildren from rooting them. + setStackPropertyAlreadyMaterialized(); +} - // static_assert(!std::is_base_of::value, "Interface is not marked as [ActiveDOMObject] even though implementation class subclasses ActiveDOMObject."); +// ErrorInstance leaves .stack unset on an empty trace; other engines still give the header line. +void JSDOMException::putHeaderStackIfNoFrames(VM& vm) +{ + auto* trace = stackTrace(); + if (trace && !trace->isEmpty()) + return; + setStackString(vm, displayHeader(vm)); } +// Same joining rule as formatStackTrace: either side may be empty. +String JSDOMException::displayHeader(VM& vm) const +{ + auto name = displayName(vm); + auto message = displayMessage(vm); + if (name.isEmpty()) + return message; + if (message.isEmpty()) + return name; + return makeString(name, ": "_s, message); +} + +String JSDOMException::ownStringOr(VM& vm, PropertyName propertyName, String&& fallback) const +{ + JSValue own = getDirect(vm, propertyName); + if (!own || !own.isString()) + return WTF::move(fallback); + auto* string = asString(own); + if (string->isRope()) // Resolving would allocate; callers may be inside a finalizer. + return WTF::move(fallback); + return String(string->tryGetValue(false)); +} + +String JSDOMException::displayName(VM& vm) const +{ + return ownStringOr(vm, vm.propertyNames->name, wrapped().name()); +} + +String JSDOMException::displayMessage(VM& vm) const +{ + return ownStringOr(vm, vm.propertyNames->message, wrapped().message()); +} + +template +void JSDOMException::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + + // Heap sweeps dead frames only for vm.errorInstanceSpace(); this subspace must keep its own alive. + Locker locker { thisObject->cellLock() }; + if (auto* stackTrace = thisObject->stackTrace()) { + for (auto& frame : *stackTrace) + frame.visitAggregate(visitor); + } +} + +DEFINE_VISIT_CHILDREN(JSDOMException); + JSObject* JSDOMException::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) { return JSDOMExceptionPrototype::create(vm, &globalObject, JSDOMExceptionPrototype::createStructure(vm, &globalObject, globalObject.errorPrototype())); @@ -316,12 +381,13 @@ JSC_DEFINE_CUSTOM_GETTER(jsDOMException_message, (JSGlobalObject * lexicalGlobal JSC::GCClient::IsoSubspace* JSDOMException::subspaceForImpl(JSC::VM& vm) { - return WebCore::subspaceForImpl( + return WebCore::subspaceForImpl( vm, [](auto& spaces) { return spaces.m_clientSubspaceForDOMException.get(); }, [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForDOMException = std::forward(space); }, [](auto& spaces) { return spaces.m_subspaceForDOMException.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForDOMException = std::forward(space); }); + [](auto& spaces, auto&& space) { spaces.m_subspaceForDOMException = std::forward(space); }, + [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForJSDOMException; }); } void JSDOMException::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) @@ -350,7 +416,9 @@ void JSDOMExceptionOwner::finalize(JSC::Handle handle, void* conte JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) { - return createWrapper(globalObject, WTF::move(impl)); + auto* wrapper = createWrapper(globalObject, WTF::move(impl)); + wrapper->putHeaderStackIfNoFrames(globalObject->vm()); + return wrapper; } JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, DOMException& impl) diff --git a/src/jsc/bindings/webcore/JSDOMException.h b/src/jsc/bindings/webcore/JSDOMException.h index 41829d950462..266310635174 100644 --- a/src/jsc/bindings/webcore/JSDOMException.h +++ b/src/jsc/bindings/webcore/JSDOMException.h @@ -24,14 +24,21 @@ #include "DOMException.h" #include "JSDOMWrapper.h" +#include #include #include namespace WebCore { -class JSDOMException : public JSDOMWrapper { +// An ErrorInstance so DOMException has [[ErrorData]] and a stack, as WebIDL requires. +class JSDOMException : public JSC::ErrorInstance { public: - using Base = JSDOMWrapper; + using Base = JSC::ErrorInstance; + using DOMWrapped = DOMException; + + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + static JSDOMException* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) { JSDOMException* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSDOMException(structure, *globalObject, WTF::move(impl)); @@ -45,10 +52,11 @@ class JSDOMException : public JSDOMWrapper { static void destroy(JSC::JSCell*); DECLARE_INFO; + DECLARE_VISIT_CHILDREN; static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ErrorInstanceType, StructureFlags), info(), JSC::NonArray); } static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); @@ -61,10 +69,31 @@ class JSDOMException : public JSDOMWrapper { static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + DOMException& wrapped() const { return m_wrapped; } + Ref protectedWrapped() const { return m_wrapped; } + static constexpr ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSDOMException, m_wrapped); } + constexpr static bool hasCustomPtrTraits() { return false; } + + JSDOMGlobalObject* globalObject() const { return uncheckedDowncast(JSC::JSNonFinalObject::globalObject()); } + + // Installs `stack` as an own property and keeps the lazily captured trace from overwriting it. + void setStackString(JSC::VM&, WTF::String&&); + void putHeaderStackIfNoFrames(JSC::VM&); + + // An own data property wins over the wrapped impl, as it does for a plain Error. Never allocates or runs JS. + WTF::String displayName(JSC::VM&) const; + WTF::String displayMessage(JSC::VM&) const; + WTF::String displayHeader(JSC::VM&) const; + protected: JSDOMException(JSC::Structure*, JSDOMGlobalObject&, Ref&&); void finishCreation(JSC::VM&); + +private: + WTF::String ownStringOr(JSC::VM&, JSC::PropertyName, WTF::String&& fallback) const; + + Ref m_wrapped; }; class JSDOMExceptionOwner final : public JSC::WeakHandleOwner { diff --git a/src/jsc/bindings/webcore/SerializedScriptValue.cpp b/src/jsc/bindings/webcore/SerializedScriptValue.cpp index a6ada2d00b66..f61734f40a04 100644 --- a/src/jsc/bindings/webcore/SerializedScriptValue.cpp +++ b/src/jsc/bindings/webcore/SerializedScriptValue.cpp @@ -416,8 +416,10 @@ const uint8_t cryptoKeyOKPOpNameTagMaximumValue = 1; * Version 13. added support for ErrorInstance objects. * Version 14. Date, RegExp, Error, DOMException, CryptoKey, KeyObject, X509Certificate, * and Bun cloneable types are recorded in the object reference pool on both sides. + * Version 15. DOMException records its stack. */ -[[maybe_unused]] static constexpr unsigned CurrentVersion = 14; +[[maybe_unused]] static constexpr unsigned CurrentVersion = 15; +[[maybe_unused]] static constexpr unsigned FirstVersionWithDOMExceptionStack = 15; // Deserializers must not pool the version 14 terminal types for older payloads, // whose writers never counted them, or the pool indices stop matching the writer's. [[maybe_unused]] static constexpr unsigned FirstVersionWithPooledTerminals = 14; @@ -1077,20 +1079,6 @@ class CloneSerializer : public CloneBase { return dumpIfTerminal(toJSArrayBuffer(*arrayBuffer), code); } - void dumpDOMException(JSObject* obj, SerializationReturnCode& code) - { - if (auto* exception = JSDOMException::toWrapped(m_lexicalGlobalObject->vm(), obj)) { - if (!startObjectInternal(obj)) // handle duplicates - return; - write(DOMExceptionTag); - write(exception->message()); - write(exception->name()); - return; - } - - code = SerializationReturnCode::DataCloneError; - } - bool dumpIfTerminal(JSValue value, SerializationReturnCode& code) { if (!value.isCell()) { @@ -1183,6 +1171,25 @@ class CloneSerializer : public CloneBase { write(String::fromLatin1(JSC::Yarr::flagsString(regExp->regExp()->flags()).data())); return true; } + if (auto* domException = dynamicDowncast(obj)) { + if (!startObjectInternal(domException)) // handle duplicates + return true; + auto& vm = m_lexicalGlobalObject->vm(); + // [[Get]] runs a user prepareStackTrace; the throw propagates out of the clone like Node. + JSValue stackValue = domException->get(m_lexicalGlobalObject, vm.propertyNames->stack); + RETURN_IF_EXCEPTION(scope, false); + String stack; + if (stackValue.isString()) { + stack = stackValue.toWTFString(m_lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, false); + } + auto& impl = domException->wrapped(); + write(DOMExceptionTag); + write(impl.message()); + write(impl.name()); + writeNullableString(stack); + return true; + } if (auto* errorInstance = dynamicDowncast(obj)) { if (!startObjectInternal(errorInstance)) // handle duplicates return true; @@ -1391,10 +1398,6 @@ class CloneSerializer : public CloneBase { return true; } #endif - if (obj->inherits()) { - dumpDOMException(obj, code); - return true; - } // write bun types auto _cloneable = StructuredCloneableSerialize::fromJS(value); @@ -3461,8 +3464,13 @@ class CloneDeserializer : public CloneBase { CachedStringRef name; if (!readStringData(name)) return JSValue(); + String stack; + if (m_version >= FirstVersionWithDOMExceptionStack && !readNullableString(stack)) + return JSValue(); auto exception = DOMException::create(message->string(), name->string()); JSValue wrapper = getJSValue(exception); + if (!stack.isNull()) + uncheckedDowncast(asObject(wrapper))->setStackString(m_lexicalGlobalObject->vm(), WTF::move(stack)); addTerminalToObjectPool(wrapper); return wrapper; } diff --git a/test/js/node/domexception-node.test.js b/test/js/node/domexception-node.test.js index 6896119ebbac..ee678a0d4179 100644 --- a/test/js/node/domexception-node.test.js +++ b/test/js/node/domexception-node.test.js @@ -1,4 +1,6 @@ import { describe, expect, it } from "bun:test"; +import { bunEnv, bunExe } from "harness"; +import { inspect } from "node:util"; describe("DOMException in Node.js environment", () => { it("exists globally", () => { @@ -56,13 +58,195 @@ describe("DOMException in Node.js environment", () => { expect(DOMException.DATA_CLONE_ERR).toBe(25); }); - // TODO: missing stack trace on DOMException - it.failing("inherits prototype properties from Error", () => { + it("inherits prototype properties from Error", () => { const error = new DOMException("Test error"); expect(error.toString()).toBe("Error: Test error"); expect(error.stack).toBeDefined(); }); + it("has [[ErrorData]] internal slot", () => { + const error = new DOMException("boom", "AbortError"); + expect(Error.isError(error)).toBe(true); + expect(Object.prototype.toString.call(error)).toBe("[object DOMException]"); + }); + + it("captures a stack trace", () => { + function inner() { + return new DOMException("boom", "AbortError"); + } + const error = inner(); + expect(typeof error.stack).toBe("string"); + expect(error.stack).toStartWith("AbortError: boom"); + expect(error.stack).toContain("inner"); + expect(Object.getOwnPropertyNames(error)).toContain("stack"); + }); + + it("keeps name/message/code as prototype accessors", () => { + const error = new DOMException("boom", "AbortError"); + const own = Object.getOwnPropertyNames(error); + expect(own).not.toContain("name"); + expect(own).not.toContain("message"); + expect(own).not.toContain("code"); + + const proto = Object.getPrototypeOf(error); + expect(Object.getOwnPropertyDescriptor(proto, "name").get).toBeFunction(); + expect(Object.getOwnPropertyDescriptor(proto, "message").get).toBeFunction(); + expect(Object.getOwnPropertyDescriptor(proto, "code").get).toBeFunction(); + }); + + it("AbortSignal.abort().reason is a DOMException with a stack", () => { + const reason = AbortSignal.abort().reason; + expect(reason).toBeInstanceOf(DOMException); + expect(Error.isError(reason)).toBe(true); + expect(reason.name).toBe("AbortError"); + expect(typeof reason.stack).toBe("string"); + expect(reason.stack).toStartWith("AbortError"); + }); + + it("AbortSignal.timeout() reason is a DOMException with a stack", async () => { + // AbortSignal.timeout()'s timer is not ref'd and awaiting only its abort + // event hangs the Windows test runner, so drive the loop with a ref'd + // sleep and poll the aborted flag (same shape as web/abort/abort.test.ts). + const signal = AbortSignal.timeout(0); + for (let i = 0; i < 200 && !signal.aborted; i++) await Bun.sleep(10); + expect(signal.aborted).toBe(true); + const reason = signal.reason; + expect(reason).toBeInstanceOf(DOMException); + expect(Error.isError(reason)).toBe(true); + expect(reason.name).toBe("TimeoutError"); + expect(typeof reason.stack).toBe("string"); + expect(reason.stack).toStartWith("TimeoutError: The operation timed out."); + }); + + it("gets a header-only stack when no frames are captured", async () => { + // Error.stackTraceLimit is process-global, so mutate it in a subprocess. + // `undefined` makes getStackTrace return no trace; `0` returns an empty one. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `class Sub extends DOMException {} + Error.stackTraceLimit = undefined; + console.log(JSON.stringify(new DOMException("boom", "AbortError").stack)); + Error.stackTraceLimit = 0; + console.log(JSON.stringify(new DOMException("boom", "AbortError").stack)); + console.log(JSON.stringify(new DOMException("", "AbortError").stack)); + console.log(JSON.stringify(new DOMException("boom", "").stack)); + console.log(JSON.stringify(new Sub("boom", "AbortError").stack));`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: '"AbortError: boom"\n"AbortError: boom"\n"AbortError"\n"boom"\n"AbortError: boom"\n', + stderr: "", + exitCode: 0, + }); + }); + + it("AbortController.abort() reason is a DOMException with a stack", () => { + const c = new AbortController(); + c.abort(); + const reason = c.signal.reason; + expect(reason).toBeInstanceOf(DOMException); + expect(Error.isError(reason)).toBe(true); + expect(reason.name).toBe("AbortError"); + expect(typeof reason.stack).toBe("string"); + expect(reason.stack).toStartWith("AbortError"); + }); + + it("structuredClone preserves a DOMException including its stack", () => { + const original = new DOMException("boom", "QuotaExceededError"); + const clone = structuredClone(structuredClone(original)); + expect(clone).toBeInstanceOf(DOMException); + expect(Error.isError(clone)).toBe(true); + expect(clone.stack).toContain("domexception-node.test.js"); + expect({ name: clone.name, message: clone.message, code: clone.code, stack: clone.stack }).toEqual({ + name: "QuotaExceededError", + message: "boom", + code: 22, + stack: original.stack, + }); + // The clone's own capture at the structuredClone call site is discarded: reading the + // position properties must not materialize the clone site's line/column over the copy. + (void clone.line, clone.column, clone.sourceURL); + expect(Object.getOwnPropertyNames(clone)).toEqual(["stack"]); + }); + + it("works with Error.captureStackTrace", () => { + function frameName() { + const error = new DOMException("boom", "NetworkError"); + Error.captureStackTrace(error); + return error; + } + const error = frameName(); + expect(typeof error.stack).toBe("string"); + expect(error.stack).toStartWith("NetworkError: boom"); + expect(error.stack).toContain("frameName"); + }); + + it("stack header omits the separator when the message is empty", () => { + expect(new DOMException().stack.split("\n")[0]).toBe("Error"); + expect(new DOMException("", "AbortError").stack.split("\n")[0]).toBe("AbortError"); + }); + + it("own name/message properties win in the stack header, like a plain Error", () => { + const define = error => { + Object.defineProperty(error, "name", { value: "CustomError", configurable: true }); + Object.defineProperty(error, "message", { value: "custom message", configurable: true }); + return error.stack.split("\n")[0]; + }; + expect(define(new DOMException("boom", "AbortError"))).toBe(define(new Error("boom"))); + }); + + it("own name property wins in the uncaught exception report", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const error = new DOMException("boom", "AbortError"); + Object.defineProperty(error, "name", { value: "CustomError" }); + throw error;`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // The source line echoed above the report still says AbortError; only the report line must not. + expect(stderr).not.toContain("\nAbortError: boom\n"); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "", + stderr: expect.stringContaining("\nCustomError: boom\n"), + exitCode: 1, + }); + }); + + it("util.inspect shows the error name and message", () => { + const error = new DOMException("boom", "AbortError"); + const inspected = inspect(error); + expect(inspected).toStartWith("DOMException [AbortError]: boom"); + }); + + it("survives GC with intact stack traces", () => { + function makeError() { + return new DOMException("gc test", "AbortError"); + } + const errors = []; + for (let i = 0; i < 100; i++) errors.push(makeError()); + Bun.gc(true); + for (const e of errors) { + expect(Error.isError(e)).toBe(true); + expect(typeof e.stack).toBe("string"); + expect(e.stack).toContain("makeError"); + } + Bun.gc(true); + for (const e of errors) { + expect(e.name).toBe("AbortError"); + expect(e.message).toBe("gc test"); + } + }); + it("has proper instance properties", () => { const error = new DOMException("Test error", "TestName"); expect(error.code).toBe(0); // Default code for custom names