docs

DOM Handles and Errors

Store DOM handles safely across navigations and handle DOM errors as values.

On this page

DOM handles are safe references to elements on a page— you can store them in your own objects and use them later without crashes when the page goes away. When a DOM operation fails, the library returns an error as a value instead of throwing a C++ exception.

How Handles Work

A DOM handle is a reference to an underlying object on the page. Copying a handle creates another reference to the same element rather than duplicating the element itself.

Handle States

A handle is always in one of three states, depending on whether it holds an element and whether its page is still alive.

State When a handle is in it if (el) Calls on it Calls with dom::Checked
Valid Points to an element on a live page true Work normally Succeed
Empty A lookup found no match, or the handle was never assigned or was moved from false Do nothing Fail with an error where is_empty() is true
Gone Its page navigated away, its frame was removed, or its View was destroyed false Do nothing Fail with an error where is_page_gone() is true

Calls on an empty handle or a handle whose page is gone fail safely— changes are ignored, and reads return an empty value.

Checking a handle with if (el) calls IsAlive(), which returns false for both an empty handle and a handle whose page is gone. To tell them apart, call IsEmpty()— it returns true only for an empty handle.

Storing a Handle

You can store a handle as a class member and update it later when your application state changes.

C++
class HUD : public LoadListener {
 public:
  void OnDOMReady(View* caller, uint64_t frame_id, bool is_main_frame,
                  const String& url) override {
    if (!is_main_frame)
      return;
    dom::Document document(caller);
    score_ = document.getElementById("score");
  }

  void SetScore(int score) {
    score_.textContent = std::to_string(score);
  }

 private:
  dom::Element score_;
};

After a navigation, SetScore() does nothing until OnDOMReady() fetches the new page's element.

🚧 Get Fresh Handles for Each Page

A handle never keeps its page alive. Once a page navigates away, its handles never become valid again, even if that page comes back from the back-forward cache.

Chaining Calls Without Checks

A lookup that finds nothing returns an empty handle— you can chain calls without null checks.

C++
document.querySelector("#save").click();

If #save isn't on the page, click() does nothing.

Handling Errors

A call that fails returns an empty value (an empty element or list, false, or nothing).

When diagnostics are on, the library logs the DOM error behind the failure, such as a malformed selector or an invalid tree change (see Finding Silent Mistakes below).

To get the reason in code, pass dom::Checked as an extra argument to return a dom::Result (the value or a dom::Error).

C++
dom::Result<dom::Element> total =
    document.querySelector("#scores td.total", dom::Checked);
if (!total) {
  if (total.error().is_page_gone())
    return;
  Log(total.error().message());
}

A page can navigate away at any moment— you can usually ignore a page-gone error.

For other errors, log message() to see why the call failed.

If is_empty() is true, the call was made on an empty handle or received one as an argument— that usually points to a bug in native code.

📘 Empty Values Are Often Enough

Most code never needs dom::Checked— the empty value is enough.

🚧 Stay on the Renderer's Thread

You must call DOM operations on the Renderer's thread. Copying, moving, and destroying handles is safe from any thread.

Finding Silent Mistakes

Safe chaining avoids crashes, but a typo in a selector fails silently— the lookup matches nothing, and subsequent calls do nothing without reporting an error. You can configure what the library logs about DOM operations through Config::diagnostics.dom, which writes warnings to the Logger tagged [dom].

Level What the DOM API Logs
DiagnosticsLevel::Off Logs nothing for individual calls, but still reports mistakes detected once (eg, a malformed listener selector)
DiagnosticsLevel::Warn Logs DOM errors from calls without dom::Checked (eg, a malformed selector or rejected property write) and warns once per page about calls on a handle whose page is gone. Calls on an empty handle remain silent
DiagnosticsLevel::Strict Logs everything reported by Warn, plus every call on an empty handle or a handle whose page is gone
DiagnosticsLevel::Auto The default. Acts like Warn when developer mode is on, and Off when it's off

When a call does nothing and the cause isn't clear (eg, a misspelled selector), set the DOM diagnostics level to Strict before creating the Renderer:

C++
Config config;
config.diagnostics.dom = DiagnosticsLevel::Strict;

Developer mode in Config switches Auto to Warn. It's configured in native code, so changing it requires recompiling. To override the level without recompiling, set the UL_DOM_DIAGNOSTICS environment variable— the library honors it only while developer mode is on. For details on both settings, see Developer Mode and Diagnostics.