diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp
index 019b91ffd94..868536230e6 100644
--- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp
+++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp
@@ -2,6 +2,7 @@
* Copyright (c) 2022, Andrew Kaster
* Copyright (c) 2023, Linus Groh
* Copyright (c) 2023, Luke Wilde
+ * Copyright (c) 2025, Shannon Booth
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -718,99 +719,151 @@ void WindowOrWorkerGlobalScopeMixin::report_error(JS::Value e)
report_an_exception(e);
}
-// https://html.spec.whatwg.org/multipage/webappapis.html#report-an-exception
-void WindowOrWorkerGlobalScopeMixin::report_an_exception(JS::Value const& e)
+// https://html.spec.whatwg.org/multipage/webappapis.html#extract-error
+struct ErrorInformation {
+ String message;
+ String filename;
+ JS::Value error;
+ size_t lineno { 0 };
+ size_t colno { 0 };
+};
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#extract-error
+static ErrorInformation extract_error_information(JS::VM& vm, JS::Value exception)
{
- auto& target = static_cast(this_impl());
- auto& realm = relevant_realm(target);
- auto& vm = realm.vm();
- auto script_or_module = vm.get_active_script_or_module();
+ // 1. Let attributes be an empty map keyed by IDL attributes.
+ ErrorInformation attributes;
- // FIXME: Get the current position in the script.
- auto line = 0;
- auto col = 0;
+ // 2. Set attributes[error] to exception.
+ attributes.error = exception;
- // 1. If target is in error reporting mode, then return; the error is not handled.
- if (m_error_reporting_mode) {
- report_exception_to_console(e, realm, ErrorInPromise::No);
- return;
- }
-
- // 2. Let target be in error reporting mode.
- m_error_reporting_mode = true;
-
- // 3. Let message be an implementation-defined string describing the error in a helpful manner.
- auto message = [&] {
- if (e.is_object()) {
- auto& object = e.as_object();
+ // 3. Set attributes[message], attributes[filename], attributes[lineno], and attributes[colno] to
+ // implementation-defined values derived from exception.
+ attributes.message = [&] {
+ if (exception.is_object()) {
+ auto& object = exception.as_object();
if (MUST(object.has_own_property(vm.names.message))) {
auto message = object.get_without_side_effects(vm.names.message);
return message.to_string_without_side_effects();
}
}
- return MUST(String::formatted("Uncaught exception: {}", e.to_string_without_side_effects()));
+ return MUST(String::formatted("Uncaught exception: {}", exception.to_string_without_side_effects()));
}();
- // 4. Let errorValue be the value that represents the error: in the case of an uncaught exception,
- // that would be the value that was thrown; in the case of a JavaScript error that would be an Error object
- // If there is no corresponding value, then the null value must be used instead.
- auto error_value = e;
+ // FIXME: This offset is relative to the javascript source. Other browsers appear to do it relative
+ // to the entire source document! Calculate that somehow.
- // 5. Let urlString be the result of applying the URL serializer to the URL record that corresponds to the resource from which script was obtained.
- // NOTE: urlString is set below once we have determined whether we are dealing with a script or a module.
- String url_string;
- auto script_or_module_filename = [](auto const& script_or_module) {
- return MUST(String::from_utf8(script_or_module->filename()));
- };
+ // If we got an Error object, then try and extract the information from the location the object was made.
+ if (exception.is_object() && is(exception.as_object())) {
+ auto const& error = static_cast(exception.as_object());
+ for (auto const& frame : error.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()));
+ attributes.lineno = source_range.start.line;
+ attributes.colno = source_range.start.column;
+ break;
+ }
+ }
+ }
+ // Otherwise, we fall back to try and find the location of the invocation of the function itself.
+ else {
+ for (ssize_t i = vm.execution_context_stack().size() - 1; i >= 0; --i) {
+ auto& frame = vm.execution_context_stack()[i];
+ if (frame->executable && frame->program_counter.has_value()) {
+ auto source_range = frame->executable->source_range_at(frame->program_counter.value()).realize();
+ attributes.filename = MUST(String::from_byte_string(source_range.filename()));
+ attributes.lineno = source_range.start.line;
+ attributes.colno = source_range.start.column;
+ break;
+ }
+ }
+ }
- // 6. If script is a classic script and script's muted errors is true, then set message to "Script error.",
- // urlString to the empty string, line and col to 0, and errorValue to null.
+ // 4. Return attributes.
+ return attributes;
+}
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#report-an-exception
+void WindowOrWorkerGlobalScopeMixin::report_an_exception(JS::Value exception, OmitError omit_error)
+{
+ auto& target = static_cast(this_impl());
+ auto& realm = relevant_realm(target);
+ auto& vm = realm.vm();
+
+ // 1. Let notHandled be true.
+ bool not_handled = true;
+
+ // 2. Let errorInfo be the result of extracting error information from exception.
+ auto error_info = extract_error_information(vm, exception);
+
+ // 3. Let script be a script found in an implementation-defined way, or null. This should usually be the
+ // running script (most notably during run a classic script).
+ auto script_or_module = vm.get_active_script_or_module();
+
+ // 4. If script is a classic script and script's muted errors is true, then set errorInfo[error] to null,
+ // errorInfo[message] to "Script error.", errorInfo[filename] to the empty string, errorInfo[lineno] to
+ // 0, and errorInfo[colno] to 0.
script_or_module.visit(
[&](GC::Ref const& js_script) {
if (verify_cast(js_script->host_defined())->muted_errors() == ClassicScript::MutedErrors::Yes) {
- message = "Script error."_string;
- url_string = String {};
- line = 0;
- col = 0;
- error_value = JS::js_null();
- } else {
- url_string = script_or_module_filename(js_script);
+ error_info.error = JS::js_null();
+ error_info.message = "Script error."_string;
+ error_info.filename = String {};
+ error_info.lineno = 0;
+ error_info.colno = 0;
}
},
- [&](GC::Ref const& js_module) {
- url_string = script_or_module_filename(js_module);
- },
- [](Empty) {});
+ [](auto const&) {});
- // 7. Let notHandled be true.
- auto not_handled = true;
+ // 5. If omitError is true, then set errorInfo[error] to null.
+ if (omit_error == OmitError::Yes)
+ error_info.error = JS::js_null();
- // 8. If target implements EventTarget, then set notHandled to the result of firing an event named error at target,
- // using ErrorEvent, with the cancelable attribute initialized to true, the message attribute initialized to message,
- // the filename attribute initialized to urlString, the lineno attribute initialized to line, the colno attribute initialized to col,
- // and the error attribute initialized to errorValue.
- ErrorEventInit event_init = {};
- event_init.cancelable = true;
- event_init.message = message;
- event_init.filename = url_string;
- event_init.lineno = line;
- event_init.colno = col;
- event_init.error = error_value;
+ // 6. If global is not in error reporting mode, then:
+ if (!m_error_reporting_mode) {
+ // 1. Set global's in error reporting mode to true.
+ m_error_reporting_mode = true;
- not_handled = target.dispatch_event(ErrorEvent::create(realm, EventNames::error, event_init));
+ // 2. If global implements EventTarget, then set notHandled to the result of firing an event named
+ // error at global, using ErrorEvent, with the cancelable attribute initialized to true, and
+ // additional attributes initialized according to errorInfo.
+ ErrorEventInit event_init = {};
+ event_init.cancelable = true;
+ event_init.message = error_info.message;
+ event_init.filename = error_info.filename;
+ event_init.lineno = error_info.lineno;
+ event_init.colno = error_info.colno;
+ event_init.error = error_info.error;
- // 9. Let target no longer be in error reporting mode.
- m_error_reporting_mode = false;
+ not_handled = target.dispatch_event(ErrorEvent::create(realm, EventNames::error, event_init));
- // 10. If notHandled is false, then the error is handled. Otherwise, the error is not handled.
+ // 3. Set global's in error reporting mode to false.
+ m_error_reporting_mode = false;
+ }
+
+ // 7. If notHandled is true, then:
if (not_handled) {
- // When the user agent is to report an exception E, the user agent must report the error for the relevant script,
- // with the problematic position (line number and column number) in the resource containing the script,
- // using the global object specified by the script's settings object as the target.
- // If the error is still not handled after this, then the error may be reported to a developer console.
- // https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception
- report_exception_to_console(e, realm, ErrorInPromise::No);
+ // 1. Set errorInfo[error] to null.
+ error_info.error = JS::js_null();
+
+ // FIXME: 2. If global implements DedicatedWorkerGlobalScope, queue a global task on the DOM manipulation
+ // task source with the global's associated Worker's relevant global object to run these steps:
+ if (false) {
+ // FIXME: 1. Let workerObject be the Worker object associated with global.
+
+ // FIXME: 2. Set notHandled be the result of firing an event named error at workerObject, using ErrorEvent,
+ // with the cancelable attribute initialized to true, and additional attributes initialized
+ // according to errorInfo.
+
+ // FIXME: 3. If notHandled is true, then report exception for workerObject's relevant global object with
+ // omitError set to true.
+ }
+ }
+ // 8. Otherwise, the user agent may report exception to a developer console.
+ else {
+ report_exception_to_console(exception, realm, ErrorInPromise::No);
}
}
diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h
index b9582733269..8ae9f2cb994 100644
--- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h
+++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h
@@ -72,7 +72,12 @@ public:
GC::Ref indexed_db();
void report_error(JS::Value e);
- void report_an_exception(JS::Value const& e);
+
+ enum class OmitError {
+ Yes,
+ No,
+ };
+ void report_an_exception(JS::Value exception, OmitError = OmitError::No);
[[nodiscard]] GC::Ref crypto();
diff --git a/Tests/LibWeb/Text/expected/HTML/WindowOrWorkerGlobalScope-reportError.txt b/Tests/LibWeb/Text/expected/HTML/WindowOrWorkerGlobalScope-reportError.txt
index c7a49e8bc1d..49848a232a6 100644
--- a/Tests/LibWeb/Text/expected/HTML/WindowOrWorkerGlobalScope-reportError.txt
+++ b/Tests/LibWeb/Text/expected/HTML/WindowOrWorkerGlobalScope-reportError.txt
@@ -1,6 +1,6 @@
message = Reporting an Error!
-lineno = 0
-colno = 0
+lineno = 17
+colno = 28
error = Error: Reporting an Error!
filename URL scheme = file:
filename URL final path segment = WindowOrWorkerGlobalScope-reportError.html
diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/reporterror.any.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/reporterror.any.txt
new file mode 100644
index 00000000000..3fed7791e7a
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/reporterror.any.txt
@@ -0,0 +1,10 @@
+Harness status: OK
+
+Found 5 tests
+
+5 Pass
+Pass self.reportError(1)
+Pass self.reportError(TypeError)
+Pass self.reportError(undefined)
+Pass self.reportError() (without arguments) throws
+Pass self.reportError() doesn't invoke getters
\ No newline at end of file
diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/reporterror.any.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/reporterror.any.html
new file mode 100644
index 00000000000..7493308e2dd
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/reporterror.any.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/reporterror.any.js b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/reporterror.any.js
new file mode 100644
index 00000000000..b9e7ba25bc6
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/reporterror.any.js
@@ -0,0 +1,49 @@
+setup({ allow_uncaught_exception:true });
+
+[
+ 1,
+ new TypeError(),
+ undefined
+].forEach(throwable => {
+ test(t => {
+ let happened = false;
+ self.addEventListener("error", t.step_func(e => {
+ assert_true(e.message !== "");
+ assert_equals(e.filename, new URL("reporterror.any.js", location.href).href);
+ assert_greater_than(e.lineno, 0);
+ assert_greater_than(e.colno, 0);
+ assert_equals(e.error, throwable);
+ happened = true;
+ }), { once:true });
+ self.reportError(throwable);
+ assert_true(happened);
+ }, `self.reportError(${throwable})`);
+});
+
+test(() => {
+ assert_throws_js(TypeError, () => self.reportError());
+}, `self.reportError() (without arguments) throws`);
+
+test(() => {
+ // Workaround for https://github.com/web-platform-tests/wpt/issues/32105
+ let invoked = false;
+ self.reportError({
+ get name() {
+ invoked = true;
+ assert_unreached('get name')
+ },
+ get message() {
+ invoked = true;
+ assert_unreached('get message');
+ },
+ get fileName() {
+ invoked = true;
+ assert_unreached('get fileName');
+ },
+ get lineNumber() {
+ invoked = true;
+ assert_unreached('get lineNumber');
+ }
+ });
+ assert_false(invoked);
+}, `self.reportError() doesn't invoke getters`);