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.

```cpp
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.

```cpp
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`).

```cpp
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:

```cpp
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](/docs/2.0/developer-mode-and-diagnostics).
