LibJS+LibWeb: Make DOMException hold an [[ErrorData]] slot

Split JS::ErrorData out of JS::Error so that it can be used both
by JS::Error and WebIDL::DOMException. This adds support for
Error.isError to DOMException, also letting us report DOMException
stack information to the console.
This commit is contained in:
Shannon Booth 2026-04-04 15:39:01 +02:00 committed by Shannon Booth
parent bdd9c98d44
commit 57130908b3
24 changed files with 429 additions and 152 deletions

View file

@ -79,6 +79,7 @@ set(SOURCES
Runtime/Environment.cpp
Runtime/Error.cpp
Runtime/ErrorConstructor.cpp
Runtime/ErrorData.cpp
Runtime/ErrorPrototype.cpp
Runtime/ErrorTypes.cpp
Runtime/ExecutionContext.cpp

View file

@ -753,10 +753,10 @@ void Console::output_debug_message(LogLevel log_level, StringView output) const
}
}
void Console::report_exception(JS::Error const& exception, bool in_promise) const
void Console::report_exception(String const& name, String const& message, JS::ErrorData const& error_data, bool in_promise) const
{
if (m_client)
m_client->report_exception(exception, in_promise);
m_client->report_exception(name, message, error_data, in_promise);
}
ThrowCompletionOr<String> Console::value_vector_to_string(GC::RootVector<Value> const& values)

View file

@ -97,7 +97,7 @@ public:
ThrowCompletionOr<Value> time_end();
void output_debug_message(LogLevel log_level, StringView output) const;
void report_exception(JS::Error const&, bool) const;
void report_exception(String const& name, String const& message, JS::ErrorData const&, bool) const;
private:
explicit Console(Realm&);
@ -126,7 +126,7 @@ public:
virtual ThrowCompletionOr<Value> printer(Console::LogLevel log_level, PrinterArguments) = 0;
virtual void add_css_style_to_current_message(StringView) { }
virtual void report_exception(JS::Error const&, bool) { }
virtual void report_exception(String const&, String const&, JS::ErrorData const&, bool) { }
virtual void clear() = 0;
virtual void end_group() = 0;

View file

@ -180,6 +180,7 @@ struct DisposableResource;
class ECMAScriptFunctionObject;
class Environment;
class Error;
class ErrorData;
class ErrorType;
struct ExecutionContext;
struct ExportEntry;

View file

@ -5,27 +5,14 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/ExecutionContext.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/SourceRange.h>
namespace JS {
GC_DEFINE_ALLOCATOR(Error);
static SourceRange dummy_source_range { SourceCode::create({}, {}), {}, {} };
SourceRange const& TracebackFrame::source_range() const
{
if (!cached_source_range.has_value())
return dummy_source_range;
return *cached_source_range;
}
GC::Ref<Error> Error::create(Realm& realm)
{
return realm.create<Error>(realm.intrinsics().error_prototype());
@ -43,16 +30,21 @@ GC::Ref<Error> Error::create(Realm& realm, StringView message)
return create(realm, Utf16String::from_utf8(message));
}
Utf16String Error::stack_string(CompactTraceback compact) const
{
return ErrorData::stack_string(compact);
}
Error::Error(Object& prototype)
: Object(ConstructWithPrototypeTag::Tag, prototype)
, ErrorData(prototype.vm())
{
populate_stack();
}
void Error::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_cached_string);
ErrorData::visit_edges(visitor);
}
// 20.5.8.1 InstallErrorCause ( O, options ), https://tc39.es/ecma262/#sec-installerrorcause
@ -81,83 +73,6 @@ void Error::set_message(Utf16String message)
define_direct_property(vm.names.message, PrimitiveString::create(vm, move(message)), attr);
}
void Error::populate_stack()
{
auto stack_trace = vm().stack_trace();
m_traceback.ensure_capacity(stack_trace.size());
for (auto& element : stack_trace) {
auto* context = element.execution_context;
m_traceback.append({
.function_name = context->function ? context->function->name_for_call_stack() : ""_utf16,
.cached_source_range = move(element.source_range),
});
}
}
Utf16String Error::stack_string(CompactTraceback compact) const
{
if (m_traceback.is_empty())
return {};
StringBuilder stack_string_builder(StringBuilder::Mode::UTF16);
// Note: We roughly follow V8's formatting
auto append_frame = [&](TracebackFrame const& frame) {
auto const& function_name = frame.function_name;
auto const& source_range = frame.source_range();
// Note: Since we don't know whether we have a valid SourceRange here we just check for some default values.
if (!source_range.filename().is_empty() || source_range.start.offset != 0 || source_range.end.offset != 0) {
if (function_name.is_empty())
stack_string_builder.appendff(" at {}:{}:{}\n", source_range.filename(), source_range.start.line, source_range.start.column);
else
stack_string_builder.appendff(" at {} ({}:{}:{})\n", function_name, source_range.filename(), source_range.start.line, source_range.start.column);
} else {
stack_string_builder.appendff(" at {}\n", function_name.is_empty() ? "<unknown>"_utf16 : function_name);
}
};
auto is_same_frame = [](TracebackFrame const& a, TracebackFrame const& b) {
if (a.function_name.is_empty() && b.function_name.is_empty()) {
auto const& source_range_a = a.source_range();
auto const& source_range_b = b.source_range();
return source_range_a.filename() == source_range_b.filename() && source_range_a.start.line == source_range_b.start.line;
}
return a.function_name == b.function_name;
};
// Note: We don't want to capture the global execution context, so we omit the last frame
// Note: The error's name and message get prepended by ErrorPrototype::stack
// FIXME: We generate a stack-frame for the Errors constructor, other engines do not
unsigned repetitions = 0;
size_t used_frames = m_traceback.size() - 1;
for (size_t i = 0; i < used_frames; ++i) {
auto const& frame = m_traceback[i];
if (compact == CompactTraceback::Yes && i + 1 < used_frames) {
auto const& next_traceback_frame = m_traceback[i + 1];
if (is_same_frame(frame, next_traceback_frame)) {
repetitions++;
continue;
}
}
if (repetitions > 4) {
// If more than 5 (1 + >4) consecutive function calls with the same name, print
// the name only once and show the number of repetitions instead. This prevents
// printing ridiculously large call stacks of recursive functions.
append_frame(frame);
stack_string_builder.appendff(" {} more calls\n", repetitions);
} else {
for (size_t j = 0; j < repetitions + 1; j++)
append_frame(frame);
}
repetitions = 0;
}
for (size_t j = 0; j < repetitions; j++)
append_frame(m_traceback[used_frames - 1]);
return stack_string_builder.to_utf16_string();
}
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, ArrayType) \
GC_DEFINE_ALLOCATOR(ClassName); \
GC::Ref<ClassName> ClassName::create(Realm& realm) \

View file

@ -12,24 +12,14 @@
#include <AK/Utf16String.h>
#include <LibJS/Export.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/ErrorData.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/SourceRange.h>
namespace JS {
struct JS_API TracebackFrame {
Utf16String function_name;
[[nodiscard]] SourceRange const& source_range() const;
Optional<SourceRange> cached_source_range;
};
enum CompactTraceback {
No,
Yes,
};
class JS_API Error : public Object {
class JS_API Error
: public Object
, public ErrorData {
JS_OBJECT(Error, Object);
GC_DECLARE_ALLOCATOR(Error);
@ -46,11 +36,6 @@ public:
void set_message(Utf16String);
Vector<TracebackFrame, 32> const& traceback() const { return m_traceback; }
void set_cached_string(GC::Ref<PrimitiveString> string) { m_cached_string = string; }
GC::Ptr<PrimitiveString> cached_string() const { return m_cached_string; }
protected:
explicit Error(Object& prototype);
@ -58,11 +43,8 @@ protected:
private:
virtual bool is_error_object() const final { return true; }
void populate_stack();
Vector<TracebackFrame, 32> m_traceback;
GC::Ptr<PrimitiveString> m_cached_string;
virtual ErrorData* error_data() final { return this; }
virtual ErrorData const* error_data() const final { return this; }
};
template<>

View file

@ -127,9 +127,16 @@ JS_ENUMERATE_NATIVE_ERRORS
JS_DEFINE_NATIVE_FUNCTION(ErrorConstructor::is_error)
{
// 1. If arg is not an Object, return false.
auto object = vm.argument(0).as_if<Object>();
if (!object)
return false;
// 2. If arg does not have an [[ErrorData]] internal slot, return false.
if (!object->has_error_data())
return false;
// 3. Return true.
return vm.argument(0).is<Error>();
return true;
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2020-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibJS/Runtime/ErrorData.h>
#include <LibJS/Runtime/ExecutionContext.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/PrimitiveString.h>
#include <LibJS/Runtime/VM.h>
namespace JS {
static SourceRange dummy_source_range { SourceCode::create({}, {}), {}, {} };
SourceRange const& TracebackFrame::source_range() const
{
if (!cached_source_range.has_value())
return dummy_source_range;
return *cached_source_range;
}
ErrorData::ErrorData(VM& vm)
{
populate_stack(vm);
}
void ErrorData::visit_edges(Cell::Visitor& visitor)
{
visitor.visit(m_cached_string);
}
void ErrorData::populate_stack(VM& vm)
{
auto stack_trace = vm.stack_trace();
m_traceback.ensure_capacity(stack_trace.size());
for (auto& element : stack_trace) {
auto* context = element.execution_context;
m_traceback.append({
.function_name = context->function ? context->function->name_for_call_stack() : ""_utf16,
.cached_source_range = move(element.source_range),
});
}
}
Utf16String ErrorData::stack_string(CompactTraceback compact) const
{
if (m_traceback.is_empty())
return {};
StringBuilder stack_string_builder(StringBuilder::Mode::UTF16);
// Note: We roughly follow V8's formatting
auto append_frame = [&](TracebackFrame const& frame) {
auto const& function_name = frame.function_name;
auto const& source_range = frame.source_range();
// Note: Since we don't know whether we have a valid SourceRange here we just check for some default values.
if (!source_range.filename().is_empty() || source_range.start.offset != 0 || source_range.end.offset != 0) {
if (function_name.is_empty())
stack_string_builder.appendff(" at {}:{}:{}\n", source_range.filename(), source_range.start.line, source_range.start.column);
else
stack_string_builder.appendff(" at {} ({}:{}:{})\n", function_name, source_range.filename(), source_range.start.line, source_range.start.column);
} else {
stack_string_builder.appendff(" at {}\n", function_name.is_empty() ? "<unknown>"_utf16 : function_name);
}
};
auto is_same_frame = [](TracebackFrame const& a, TracebackFrame const& b) {
if (a.function_name.is_empty() && b.function_name.is_empty()) {
auto const& source_range_a = a.source_range();
auto const& source_range_b = b.source_range();
return source_range_a.filename() == source_range_b.filename() && source_range_a.start.line == source_range_b.start.line;
}
return a.function_name == b.function_name;
};
// Note: We don't want to capture the global execution context, so we omit the last frame
// Note: The error's name and message get prepended by Error.prototype.stack
unsigned repetitions = 0;
size_t used_frames = m_traceback.size() - 1;
for (size_t i = 0; i < used_frames; ++i) {
auto const& frame = m_traceback[i];
if (compact == CompactTraceback::Yes && i + 1 < used_frames) {
auto const& next_traceback_frame = m_traceback[i + 1];
if (is_same_frame(frame, next_traceback_frame)) {
repetitions++;
continue;
}
}
if (repetitions > 4) {
// If more than 5 (1 + >4) consecutive function calls with the same name, print
// the name only once and show the number of repetitions instead. This prevents
// printing ridiculously large call stacks of recursive functions.
append_frame(frame);
stack_string_builder.appendff(" {} more calls\n", repetitions);
} else {
for (size_t j = 0; j < repetitions + 1; j++)
append_frame(frame);
}
repetitions = 0;
}
for (size_t j = 0; j < repetitions; j++)
append_frame(m_traceback[used_frames - 1]);
return stack_string_builder.to_utf16_string();
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Utf16String.h>
#include <AK/Vector.h>
#include <LibGC/Ptr.h>
#include <LibJS/Export.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibJS/SourceRange.h>
namespace JS {
struct JS_API TracebackFrame {
Utf16String function_name;
[[nodiscard]] SourceRange const& source_range() const;
Optional<SourceRange> cached_source_range;
};
enum CompactTraceback {
No,
Yes,
};
class JS_API ErrorData {
public:
explicit ErrorData(VM&);
[[nodiscard]] Utf16String stack_string(CompactTraceback compact = CompactTraceback::No) const;
[[nodiscard]] Vector<TracebackFrame, 32> const& traceback() const { return m_traceback; }
void set_cached_string(GC::Ref<PrimitiveString> string) { m_cached_string = string; }
[[nodiscard]] GC::Ptr<PrimitiveString> cached_string() const { return m_cached_string; }
protected:
void visit_edges(Cell::Visitor&);
private:
void populate_stack(VM&);
Vector<TracebackFrame, 32> m_traceback;
GC::Ptr<PrimitiveString> m_cached_string;
};
}

View file

@ -78,35 +78,35 @@ JS_DEFINE_NATIVE_FUNCTION(ErrorPrototype::stack_getter)
auto this_object = TRY(PrototypeObject::this_object(vm));
// 3. If E does not have an [[ErrorData]] internal slot, return undefined.
auto* error = as_if<Error>(*this_object);
if (!error)
auto* error_data = this_object->error_data();
if (!error_data)
return js_undefined();
// OPTIMIZATION: Avoid recomputing the stack string if we already have it cached.
// At least one major engine does this as well, so it's not expected that changing
// the name or message properties updates the stack string.
if (error->cached_string())
return error->cached_string();
if (error_data->cached_string())
return error_data->cached_string();
// 4. Return ? GetStackString(error).
// NOTE: These steps are not implemented based on the proposal, but to roughly follow behavior of other browsers.
String name {};
if (auto name_property = TRY(error->get(vm.names.name)); !name_property.is_undefined())
if (auto name_property = TRY(this_object->get(vm.names.name)); !name_property.is_undefined())
name = TRY(name_property.to_string(vm));
else
name = "Error"_string;
Utf16String message {};
if (auto message_property = TRY(error->get(vm.names.message)); !message_property.is_undefined())
if (auto message_property = TRY(this_object->get(vm.names.message)); !message_property.is_undefined())
message = TRY(message_property.to_utf16_string(vm));
auto header = message.is_empty()
? move(name)
: MUST(String::formatted("{}: {}", name, message));
auto string = PrimitiveString::create(vm, Utf16String::formatted("{}\n{}", header, error->stack_string()));
error->set_cached_string(string);
auto string = PrimitiveString::create(vm, Utf16String::formatted("{}\n{}", header, error_data->stack_string()));
error_data->set_cached_string(string);
return string;
}

View file

@ -263,6 +263,9 @@ public:
virtual bool is_set_object() const { return false; }
virtual bool is_map_object() const { return false; }
virtual bool is_weak_map() const { return false; }
virtual ErrorData* error_data() { return nullptr; }
virtual ErrorData const* error_data() const { return nullptr; }
bool has_error_data() const { return error_data(); }
virtual bool is_typed_array_base() const { return false; }
#define __JS_ENUMERATE(ClassName, snake_name, PrototypeName, ConstructorName, Type) \

View file

@ -160,7 +160,7 @@ JS_DEFINE_NATIVE_FUNCTION(ObjectPrototype::to_string)
else if (object->is_function())
builtin_tag = "Function"sv;
// 8. Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
else if (is<Error>(*object))
else if (object->has_error_data())
builtin_tag = "Error"sv;
// 9. Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
else if (is<BooleanObject>(*object))

View file

@ -36,8 +36,8 @@ ErrorInformation extract_error_information(JS::VM& vm, JS::Value exception)
// to the entire source document! Calculate that somehow.
// NB: If we got an Error object, then try and extract the information from the location the object was made.
if (auto error = exception.as_if<JS::Error>()) {
for (auto const& frame : error->traceback()) {
if (auto object = exception.as_if<JS::Object>(); object && object->error_data()) {
for (auto const& frame : object->error_data()->traceback()) {
auto source_range = frame.source_range();
if (source_range.start.line != 0 || source_range.start.column != 0) {
attributes.filename = MUST(String::from_byte_string(source_range.filename()));

View file

@ -35,21 +35,27 @@ void report_exception_to_console(JS::Value value, JS::Realm& realm, ErrorInPromi
} else {
dbgln("\033[31;1mUnhandled JavaScript exception{}:\033[0m [{}] {}", error_in_promise == ErrorInPromise::Yes ? " (in promise)" : "", name, message);
}
if (is<JS::Error>(object)) {
// FIXME: We should be doing this for DOMException as well
// https://webidl.spec.whatwg.org/#js-DOMException-specialness
// "Additionally, if an implementation gives native Error objects special powers or nonstandard properties (such as a stack property), it should also expose those on DOMException objects."
auto const& error_value = static_cast<JS::Error const&>(object);
dbgln("{}", error_value.stack_string(JS::CompactTraceback::Yes));
console.report_exception(error_value, error_in_promise == ErrorInPromise::Yes);
if (auto const* error_data = object.error_data()) {
String exception_name;
String exception_message;
if (auto const* exception = as_if<WebIDL::DOMException>(object)) {
exception_name = exception->name().to_string();
exception_message = MUST(exception->message().view().to_utf8());
} else {
exception_name = name.to_string_without_side_effects();
exception_message = message.to_string_without_side_effects();
}
dbgln("{}", error_data->stack_string(JS::CompactTraceback::Yes));
console.report_exception(exception_name, exception_message, *error_data, error_in_promise == ErrorInPromise::Yes);
return;
}
} else {
dbgln("\033[31;1mUnhandled JavaScript exception{}:\033[0m {}", error_in_promise == ErrorInPromise::Yes ? " (in promise)" : "", value);
}
console.report_exception(*JS::Error::create(realm, value.to_utf16_string_without_side_effects()), error_in_promise == ErrorInPromise::Yes);
auto message = value.to_string_without_side_effects();
auto error = JS::Error::create(realm, Utf16String::from_utf8(message));
console.report_exception("Error"_string, message, *error, error_in_promise == ErrorInPromise::Yes);
}
// https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception

View file

@ -30,6 +30,7 @@ GC::Ref<DOMException> DOMException::construct_impl(JS::Realm& realm, Utf16String
DOMException::DOMException(JS::Realm& realm, FlyString name, Utf16String const& message)
: PlatformObject(realm)
, ErrorData(realm.vm())
, m_name(move(name))
, m_message(message)
{
@ -37,6 +38,7 @@ DOMException::DOMException(JS::Realm& realm, FlyString name, Utf16String const&
DOMException::DOMException(JS::Realm& realm)
: PlatformObject(realm)
, ErrorData(realm.vm())
{
}
@ -48,6 +50,12 @@ void DOMException::initialize(JS::Realm& realm)
Base::initialize(realm);
}
void DOMException::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
ErrorData::visit_edges(visitor);
}
WebIDL::ExceptionOr<void> DOMException::serialization_steps(HTML::TransferDataEncoder& serialized, bool, HTML::SerializationMemory&)
{
// 1. Set serialized.[[Name]] to values name.

View file

@ -8,6 +8,7 @@
#include <AK/Utf16FlyString.h>
#include <AK/Utf16String.h>
#include <LibJS/Runtime/ErrorData.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Bindings/Serializable.h>
#include <LibWeb/Export.h>
@ -94,6 +95,7 @@ static u16 get_legacy_code_for_name(FlyString const& name)
// https://webidl.spec.whatwg.org/#idl-DOMException
class WEB_API DOMException
: public Bindings::PlatformObject
, public JS::ErrorData
, public Bindings::Serializable {
WEB_PLATFORM_OBJECT(DOMException, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(DOMException);
@ -120,8 +122,12 @@ protected:
explicit DOMException(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
private:
virtual ErrorData* error_data() final { return this; }
virtual ErrorData const* error_data() const final { return this; }
FlyString m_name;
Utf16FlyString m_message;
};

View file

@ -10,6 +10,7 @@
#include <AK/MemoryStream.h>
#include <LibJS/Print.h>
#include <LibJS/Runtime/BigInt.h>
#include <LibJS/Runtime/ErrorData.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
@ -99,17 +100,12 @@ void DevToolsConsoleClient::handle_result(JS::Value result)
m_client->did_execute_js_console_input(serialize_js_value(m_realm, result));
}
void DevToolsConsoleClient::report_exception(JS::Error const& exception, bool in_promise)
void DevToolsConsoleClient::report_exception(String const& name, String const& message, JS::ErrorData const& error_data, bool in_promise)
{
auto& vm = exception.vm();
auto name = exception.get_without_side_effects(vm.names.name);
auto message = exception.get_without_side_effects(vm.names.message);
Vector<WebView::StackFrame> trace;
trace.ensure_capacity(exception.traceback().size());
trace.ensure_capacity(error_data.traceback().size());
for (auto const& frame : exception.traceback()) {
for (auto const& frame : error_data.traceback()) {
auto const& source_range = frame.source_range();
WebView::StackFrame stack_frame;
@ -129,8 +125,8 @@ void DevToolsConsoleClient::report_exception(JS::Error const& exception, bool in
send_console_output({
.timestamp = UnixDateTime::now(),
.output = WebView::ConsoleError {
.name = name.to_string_without_side_effects(),
.message = message.to_string_without_side_effects(),
.name = name,
.message = message,
.trace = move(trace),
.inside_promise = in_promise,
},

View file

@ -29,7 +29,7 @@ private:
DevToolsConsoleClient(JS::Realm&, JS::Console&, PageClient&, ConsoleGlobalEnvironmentExtensions&);
virtual void handle_result(JS::Value) override;
virtual void report_exception(JS::Error const&, bool) override;
virtual void report_exception(String const& name, String const& message, JS::ErrorData const&, bool) override;
virtual void end_group() override { }
virtual void clear() override { }

View file

@ -0,0 +1,20 @@
Harness status: OK
Found 15 tests
15 Pass
Pass Cannot construct without new
Pass inherits from Error: prototype-side
Pass does not inherit from Error: class-side
Pass message property descriptor
Pass message getter performs brand checks (i.e. is not [LegacyLenientThis])
Pass name property descriptor
Pass name getter performs brand checks (i.e. is not [LegacyLenientThis])
Pass code property descriptor
Pass code getter performs brand checks (i.e. is not [LegacyLenientThis])
Pass code property is not affected by shadowing the name property
Pass Object.prototype.toString behavior is like other interfaces
Pass Inherits its toString() from Error.prototype
Pass toString() behavior from Error.prototype applies as expected
Pass DOMException.prototype.toString() applied to DOMException.prototype throws because of name/message brand checks
Pass If the implementation has a stack property on normal errors, it also does on DOMExceptions

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass DOMException-is-error

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js"></script>

View file

@ -0,0 +1,122 @@
// META: global=window,dedicatedworker,shadowrealm
"use strict";
test(() => {
assert_throws_js(TypeError, () => DOMException());
}, "Cannot construct without new");
test(() => {
assert_equals(Object.getPrototypeOf(DOMException.prototype), Error.prototype);
}, "inherits from Error: prototype-side");
test(() => {
assert_equals(Object.getPrototypeOf(DOMException), Function.prototype);
}, "does not inherit from Error: class-side");
test(() => {
const e = new DOMException("message", "name");
assert_false(e.hasOwnProperty("message"), "property is not own");
const propDesc = Object.getOwnPropertyDescriptor(DOMException.prototype, "message");
assert_equals(typeof propDesc.get, "function", "property descriptor is a getter");
assert_equals(propDesc.set, undefined, "property descriptor is not a setter");
assert_true(propDesc.enumerable, "property descriptor enumerable");
assert_true(propDesc.configurable, "property descriptor configurable");
}, "message property descriptor");
test(() => {
const getter = Object.getOwnPropertyDescriptor(DOMException.prototype, "message").get;
assert_throws_js(TypeError, () => getter.apply({}));
}, "message getter performs brand checks (i.e. is not [LegacyLenientThis])");
test(() => {
const e = new DOMException("message", "name");
assert_false(e.hasOwnProperty("name"), "property is not own");
const propDesc = Object.getOwnPropertyDescriptor(DOMException.prototype, "name");
assert_equals(typeof propDesc.get, "function", "property descriptor is a getter");
assert_equals(propDesc.set, undefined, "property descriptor is not a setter");
assert_true(propDesc.enumerable, "property descriptor enumerable");
assert_true(propDesc.configurable, "property descriptor configurable");
}, "name property descriptor");
test(() => {
const getter = Object.getOwnPropertyDescriptor(DOMException.prototype, "name").get;
assert_throws_js(TypeError, () => getter.apply({}));
}, "name getter performs brand checks (i.e. is not [LegacyLenientThis])");
test(() => {
const e = new DOMException("message", "name");
assert_false(e.hasOwnProperty("code"), "property is not own");
const propDesc = Object.getOwnPropertyDescriptor(DOMException.prototype, "code");
assert_equals(typeof propDesc.get, "function", "property descriptor is a getter");
assert_equals(propDesc.set, undefined, "property descriptor is not a setter");
assert_true(propDesc.enumerable, "property descriptor enumerable");
assert_true(propDesc.configurable, "property descriptor configurable");
}, "code property descriptor");
test(() => {
const getter = Object.getOwnPropertyDescriptor(DOMException.prototype, "code").get;
assert_throws_js(TypeError, () => getter.apply({}));
}, "code getter performs brand checks (i.e. is not [LegacyLenientThis])");
test(() => {
const e = new DOMException("message", "InvalidCharacterError");
assert_equals(e.code, 5, "Initially the code is set to 5");
Object.defineProperty(e, "name", {
value: "WrongDocumentError"
});
assert_equals(e.code, 5, "The code is still set to 5");
}, "code property is not affected by shadowing the name property");
test(() => {
const e = new DOMException("message", "name");
assert_equals(Object.prototype.toString.call(e), "[object DOMException]");
}, "Object.prototype.toString behavior is like other interfaces");
test(() => {
const e = new DOMException("message", "name");
assert_false(e.hasOwnProperty("toString"), "toString must not exist on the instance");
assert_false(DOMException.prototype.hasOwnProperty("toString"), "toString must not exist on DOMException.prototype");
assert_equals(typeof e.toString, "function", "toString must still exist (via Error.prototype)");
}, "Inherits its toString() from Error.prototype");
test(() => {
const e = new DOMException("message", "name");
assert_equals(e.toString(), "name: message",
"The default Error.prototype.toString() behavior must work on supplied name and message");
Object.defineProperty(e, "name", { value: "new name" });
Object.defineProperty(e, "message", { value: "new message" });
assert_equals(e.toString(), "new name: new message",
"The default Error.prototype.toString() behavior must work on shadowed names and messages");
}, "toString() behavior from Error.prototype applies as expected");
test(() => {
assert_throws_js(TypeError, () => DOMException.prototype.toString());
}, "DOMException.prototype.toString() applied to DOMException.prototype throws because of name/message brand checks");
test(() => {
let stackOnNormalErrors;
try {
throw new Error("normal error");
} catch (e) {
stackOnNormalErrors = e.stack;
}
let stackOnDOMException;
try {
throw new DOMException("message", "name");
} catch (e) {
stackOnDOMException = e.stack;
}
assert_equals(typeof stackOnDOMException, typeof stackOnNormalErrors, "The typeof values must match");
}, "If the implementation has a stack property on normal errors, it also does on DOMExceptions");

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../webidl/ecmascript-binding/es-exceptions/DOMException-is-error.any.js"></script>

View file

@ -0,0 +1,9 @@
// META: global=window,dedicatedworker,shadowrealm
'use strict';
test(function() {
// https://github.com/tc39/proposal-is-error/issues/9
// https://github.com/whatwg/webidl/pull/1421
assert_true(Error.isError(new DOMException()));
});