# How to Debug Element Resolution Failures in ego-browser: A Complete Guide

> Debug ego-browser element resolution failures. Understand transient vs permanent errors to fix timing issues or locator syntax in this complete guide.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-07-30

---

**Element resolution failures in ego-browser throw `ElementResolutionError` with a `kind` property—`transient` indicates a retryable timing issue while `permanent` signals an invalid selector or multiple matches, allowing you to pinpoint whether to retry the call or fix the locator syntax.**

When automating browser interactions with `ego-browser` from the citrolabs/ego-lite repository, element lookups can fail due to timing issues, invalid selectors, or ambiguous matches. Understanding the resolution flow implemented in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) lets you diagnose whether a failure requires a simple retry or a fundamental fix to your locator strategy. This guide walks through the systematic debugging approach used by the core maintainers, referencing actual source code paths and error classification logic.

## Understanding the Core Resolution Architecture

The resolver operates through a layered hierarchy that attempts lookups in a specific order: **refs** (backend node IDs), **role/name** combinations via the Accessibility tree, and finally **CSS/XPath/text** selectors.

### Key Resolution Functions

Three primary functions handle element discovery in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts):

- **`resolveElementCenter`** (lines 63-71) – Returns the (x, y) coordinates of an element's center, throwing `ElementResolutionError` if the element lacks a box model or cannot be found.
- **`resolveElementObjectId`** (lines 149-155) – Returns a Chrome DevTools Protocol (CDP) `objectId` for element handles, using the same resolution logic as the center helper.
- **`matchCountKind`** (lines 46-50) – Determines whether a "matched N elements" error is `transient` or `permanent` based on the count.
- **`ElementResolutionError`** (lines 4-10) – The custom error constructor that attaches the `kind` classification to every resolution failure.

### The Resolution Hierarchy

When you pass a locator to helpers like `click()` or `waitForSelector()`, the resolver evaluates it in this strict order:

1. **Ref lookup** – If the selector starts with `@`, it queries the `refMap` (managed in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)) for a cached backend node ID.
2. **Role/Name lookup** – If the locator uses `loc=role:`, it queries the Accessibility tree via `Accessibility.getFullAXTree`.
3. **Raw selector evaluation** – Falls back to JavaScript execution via `Runtime.evaluate`, using snippets generated in [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts).

## Interpreting Error Kinds: Transient vs Permanent

Every `ElementResolutionError` carries a `kind` property that dictates your debugging strategy. The classification logic resides in the resolver's error handling paths.

### Transient Failures

**Transient** errors indicate the element may become available later, typically during page load or DOM updates. These occur when:

- The element has **no box model** (not rendered or zero-sized) – thrown at lines 55-62 in [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts):
  ```typescript
  if (content.length < 8) {
    throw new ElementResolutionError(
      "Element has no box model (not rendered or zero-sized)",
      "transient",
    );
  }
  ```

- The selector matches **zero elements** but the DOM might still be hydrating.
- A **stale backend node** reference exists in the `refMap`.

### Permanent Failures

**Permanent** errors indicate the selector is fundamentally incorrect. These trigger when:

- The selector **matches multiple elements** consistently. The `matchCountKind` function (lines 46-50) classifies this:
  ```typescript
  const n = m ? Number(m[1]) : 0;
  return n > 1 ? "permanent" : "transient";
  ```

- The locator syntax is invalid or uses an **unsupported kind**.
- A CSS or XPath selector is malformed.

## Step-by-Step Debugging Workflow

Follow this systematic approach to diagnose resolution failures using the source code from citrolabs/ego-lite.

### 1. Capture and Inspect the Error

Wrap resolution calls in try-catch blocks to expose the error metadata:

```javascript
import { resolveElementCenter, ElementResolutionError } from "ego-browser";

try {
  await resolveElementCenter(cdp, sessionId, refMap, selector);
} catch (e) {
  if (e instanceof ElementResolutionError) {
    console.error(`Kind: ${e.kind}, Message: ${e.message}`);
  }
}

```

### 2. Identify the Resolution Stage

Check which lookup path was attempted:

- **Ref path**: Confirm if your selector starts with `@` and exists in `refMap`.
- **Role path**: Verify the `loc=role:` syntax and check `Accessibility.getFullAXTree` results.
- **Raw selector path**: Inspect the generated JavaScript in [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts), specifically the `buildLocatorFindJs` function.

### 3. Inspect Underlying CDP Calls

The resolver relies on three critical CDP methods that appear in your debug logs:

- **`DOM.getBoxModel`** – Fails when elements are not rendered (triggers transient errors).
- **`Accessibility.getFullAXTree`** – Powers role/name lookups.
- **`Runtime.evaluate`** – Executes CSS/XPath/text selectors generated by `buildLocatorFindJs`.

### 4. Determine Retry vs Fix Strategy

Based on the `kind` property:

- **`transient`**: Wrap the call in a retry loop. The `waitForSelector` helper in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) automatically retries transient failures until timeout.
- **`permanent`**: Revise your selector to be more specific or fix syntax errors.

### 5. Validate Locator Syntax

Use the test suite in `src/element-resolver.test.mjs` as a reference:

- **CSS selectors** must be valid for `querySelectorAll`.
- **XPath expressions** must work with `document.evaluate`.
- **Role locators** must match AX roles with optional name filters (strings, numbers, booleans, or regex).

## Common Failure Scenarios and Fixes

### Multiple Matches (Permanent)

When a CSS selector matches multiple elements, the resolver throws a permanent error. The test case at `element-resolver.test.mjs` demonstrates this behavior:

```javascript
// This throws permanent: "css:.duplicate matched 2 elements"
await resolveElementCenter(cdp, sessionId, refMap, "loc=css:.duplicate");

```

**Fix**: Add an `nth` qualifier or refine the selector to be unique.

### Degenerate Box Model (Transient)

When an element exists in the DOM but lacks layout (zero-sized or not rendered), the resolver throws a transient error at `element-resolver.ts:55-62`. The test suite explicitly verifies this does not fall back to role/name lookup:

```javascript
await assert.rejects(
  () => resolveElementCenter(cdp, undefined, refMap, "@5"),
  (error) => {
    assert.equal(error.kind, "transient");
    assert.match(error.message, /no box model/);
    return true;
  },
);

```

**Fix**: Wait for the element to render or check CSS visibility properties.

## Practical Debugging Example

This complete example demonstrates how to instrument resolution calls to capture CDP logs and error metadata:

```javascript
import { resolveElementCenter, ElementResolutionError } from "ego-browser";

async function debugResolution(selector) {
  try {
    const pt = await resolveElementCenter(cdp, sessionId, refMap, selector);
    console.log("✅ Element centre:", pt);
  } catch (e) {
    if (e instanceof ElementResolutionError) {
      console.log(`❌ ${e.kind.toUpperCase()} failure: ${e.message}`);
      
      // Inspect the CDP call that generated the error
      if (cdp.calls) {
        console.log("CDP call history:", cdp.calls);
      }
    } else {
      console.error("Unexpected error:", e);
    }
  }
}

// Debug a problematic selector
debugResolution('loc=css:.duplicate-button');

```

Running this against a selector matching multiple elements produces:

```

❌ PERMANENT failure: Locator css:.duplicate-button matched 2 elements
CDP call history: [ [ 'Runtime.evaluate', { expression: '...' }, undefined ] ]

```

## Summary

- **Element resolution** in ego-browser follows a strict hierarchy: refs → role/name → CSS/XPath/text selectors.
- **Error classification** via `ElementResolutionError.kind` distinguishes between `transient` (retryable) and `permanent` (requires selector fix) failures.
- **Transient errors** typically indicate timing issues or missing box models, resolved by waiting or retrying.
- **Permanent errors** result from invalid syntax or ambiguous selectors matching multiple elements.
- **Source references** in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) (lines 46-50, 55-62, 63-71) contain the logic for error classification and resolution paths.
- **CDP inspection** of `DOM.getBoxModel`, `Accessibility.getFullAXTree`, and `Runtime.evaluate` calls reveals exactly where lookups fail.

## Frequently Asked Questions

### What is the difference between transient and permanent element resolution errors?

**Transient errors** indicate the element might appear if you retry the operation, typically caused by the DOM still loading, stale node references, or elements without a computed box model. **Permanent errors** mean the selector is fundamentally invalid or matches multiple elements, and retrying will never succeed until you fix the locator syntax or specificity.

### How do I debug why my role locator is not finding elements?

Check that the role matches the Accessibility tree entry exactly as exposed by the browser. The resolver uses `Accessibility.getFullAXTree` and compares against the role and optional name parameters via the `axNameMatches` function. If the element is not yet in the accessibility tree or the name matcher (string, number, boolean, or regex) is too restrictive, resolution fails with a transient error.

### Where does ego-browser generate the JavaScript for CSS and XPath selectors?

The resolver generates browser-side JavaScript snippets in [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) via the `buildLocatorFindJs` function. When debugging resolution failures, inspect the `expression` parameter sent to `Runtime.evaluate` in your CDP logs to see the exact query being executed against the document.

### Why does my selector work in browser DevTools but fail in ego-browser?

DevTools often auto-wait for elements or implicitly handle multiple matches, whereas ego-browser strictly enforces single-element resolution. If your selector matches multiple elements, ego-browser throws a `permanent` error rather than selecting the first match. Add an `nth` qualifier or refine the selector to be unique, or use a ref-based approach if the element was previously captured.