# How to Debug Element Resolution Failures Using ElementResolutionError in ego-browser

> Debug element resolution failures in ego-browser by catching ElementResolutionError. Inspect the kind property transient for retries or permanent for selector issues. Fix locators effectively.

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

---

**Catch `ElementResolutionError` and inspect its `kind` property—`transient` indicates a retryable timing issue while `permanent` signals an invalid selector or ambiguous match that requires fixing the locator syntax.**

When automating browser interactions with ego-browser, element lookups can fail due to timing issues, invalid selectors, or accessibility tree mismatches. Understanding how to interpret `ElementResolutionError` and trace failures through the resolution pipeline is essential for building robust automation scripts. This guide walks through the debugging workflow using the actual source implementation in `citrolabs/ego-lite`.

## Understanding the Element Resolution Pipeline

The resolver implements a layered lookup strategy that tries **refs**, **role/name** combinations via the Accessibility tree, and raw **CSS/XPath/text** locators. Knowing which function handles your request helps isolate the failure point.

### Core Resolution Functions

- **`resolveElementCenter`** – Returns the (x,y) centre of an element or throws. Located at [`element-resolver.ts:63‑71`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L63-L71), it handles refs, role/name lookup, or falls back to a raw selector.
- **`resolveElementObjectId`** – Returns a CDP `objectId` for an element using the same resolution order. See [`element-resolver.ts:149‑155`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L149-L155).
- **`ElementResolutionError`** – The custom error type that carries a `kind` property. Defined at [`element-resolver.ts:4‑10`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L4-L10).
- **`matchCountKind`** – Decides whether a “matched *N* elements” message is transient or permanent. Found at [`element-resolver.ts:46‑50`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts#L46-L50).

## Decoding Error Kinds: Transient vs Permanent

The `kind` property classifies failures into two categories that dictate your recovery strategy.

| Kind | Meaning | Typical Triggers |
|------|---------|-----------------|
| **`transient`** | The element may appear later (e.g., page still loading, stale nodes). | No box model, zero matches, stale backend node, multiple matches that might become unique later. |
| **`permanent`** | The selector is fundamentally wrong—retrying will not help. | Invalid selector syntax, selector consistently matches > 1 element, unsupported locator kind. |

The error kind is set explicitly throughout the resolver:

**Box-model missing** results in `transient`:

```typescript
// element-resolver.ts:55‑62
if (content.length < 8) {
  throw new ElementResolutionError(
    "Element has no box model (not rendered or zero‑sized)",
    "transient",
  );
}

```

**Multiple matches** results in `permanent` via `matchCountKind`:

```typescript
// element-resolver.ts:46‑50
const n = m ? Number(m[1]) : 0;
return n > 1 ? "permanent" : "transient";

```

## Step-by-Step Debugging Workflow

### Capture and Inspect the Error

Wrap resolution calls in a try/catch block to access the `kind` and `message` properties:

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

try {
  await resolveElementCenter(cdp, sessionId, refMap, selector);
} catch (e) {
  if (e instanceof ElementResolutionError) {
    console.error(e.kind, e.message); // "transient" or "permanent"
  }
}

```

### Trace the Resolution Stage

Identify which lookup path the resolver attempted:

1. **Refs** – If the selector starts with `@`, the resolver calls `parseRef` and checks `refMap.get`.
2. **Role/Name** – If the selector uses `loc=role:`, the resolver queries the Accessibility tree via `Accessibility.getFullAXTree`.
3. **Raw Selector** – Falls back to JavaScript evaluation for CSS/XPath/text locators.

### Inspect Underlying CDP Calls

The resolver relies on three Chrome DevTools Protocol methods:

- `DOM.getBoxModel` – Provides the element’s bounding box for coordinate calculation.
- `Accessibility.getFullAXTree` – Used for role-name lookup.
- `Runtime.evaluate` – Executes generated JavaScript for CSS/XPath/text locators.

The test suite in **`element‑resolver.test.mjs`** demonstrates these pathways. For example, the “degenerate box model” test confirms the resolver does **not** fall back to role/name lookup when a box model is empty:

```javascript
// element-resolver.test.mjs:47‑73
await assert.rejects(
  () => resolveElementCenter(cdp, undefined, refMap, "@5"),
  (error) => {
    assert.ok(error instanceof ElementResolutionError);
    assert.equal(error.kind, "transient");
    assert.match(error.message, /no box model/);
    return true;
  },
);

```

### Determine Retry Strategy Based on Kind

- **Transient**: Wrap the call in a retry loop. The framework’s `waitForSelector` helper automatically retries until the element becomes ready.
- **Permanent**: You must fix the selector or locator syntax. Retrying will never succeed.

### Validate Locator Syntax

Ensure your locator matches the expected format for its type:

- **CSS** – Must be valid for `querySelectorAll`.
- **XPath** – Must be a proper expression for `document.evaluate`.
- **Role** – Must match an AX role and optionally a name (string, number, boolean, or regex). See `axNameMatches` for matching rules.

Examples from the test suite:
- CSS with multiple matches → permanent (`test "css locator matched multiple elements is permanent"`).
- Role with numeric name → works (`test "role locator matches numeric AX names"`).

## Practical Debugging Example

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

async function debug(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 caused the error
      console.log("CDP log:", cdp.calls);
    } else {
      console.error("Unexpected error:", e);
    }
  }
}

// Example: a selector matching two buttons → permanent
debug('loc=css:.duplicate');

```

Output:

```

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

```

From the log, examine the exact `Runtime.evaluate` expression generated by `buildLocatorFindJs` to adjust your selector or add an `nth` qualifier.

## Key Source Files for Deep Dives

| File | Role in Element Resolution |
|------|----------------------------|
| [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Core implementation of element lookup, error classification, and helper builders. |
| `src/element-resolver.test.mjs` | Unit tests illustrating every failure mode and expected `kind`. |
| [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | Stores the mapping from `@N` refs to backend node IDs, roles, and names. |
| [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) | Generates the browser-side JavaScript snippets used by the resolver. |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Exposes public helpers (`click`, `waitForSelector`) that internally call the resolver. |

## Summary

- **Catch `ElementResolutionError`** to access the `kind` property and failure message.
- **Interpret `kind`**: `transient` means retry (timing/DOM not ready), `permanent` means fix the selector (syntax error or ambiguous match).
- **Trace the path**: Check if the failure occurred during ref resolution, role/name lookup, or raw selector evaluation.
- **Inspect CDP logs**: Review `DOM.getBoxModel`, `Accessibility.getFullAXTree`, or `Runtime.evaluate` calls to identify the exact failure point.
- **Validate locators**: Ensure CSS, XPath, and role selectors follow the syntax expected by `buildLocatorFindJs` and `axNameMatches`.
- **Use the test suite**: Reference `element-resolver.test.mjs` to reproduce specific failure modes and verify fixes.

## Frequently Asked Questions

### What causes an ElementResolutionError to have kind "transient"?

A `transient` kind indicates the element might appear or become valid upon retry. Common triggers include missing box models (element not yet rendered), zero matches (DOM still loading), stale backend node IDs, or multiple matches that could resolve to a single element later. The resolver throws this kind explicitly when `content.length < 8` in the box model check or when `matchCountKind` determines ambiguity might resolve over time.

### How do I fix a "permanent" element resolution error?

A `permanent` error requires correcting the locator syntax or making the selector more specific. This occurs when you have invalid CSS/XPath syntax, a role lookup that consistently matches multiple elements, or an unsupported locator kind. Review the selector in the error message, check for typos in `loc=role:` or `loc=css:` prefixes, and add qualifiers like `nth` or more specific class names to ensure a unique match.

### Can I customize the retry logic for transient errors?

Yes. While helpers like `waitForSelector` internally handle retries for transient errors, you can implement custom retry loops around `resolveElementCenter` or `resolveElementObjectId`. Catch `ElementResolutionError`, check if `error.kind === "transient"`, and retry with exponential backoff until a timeout. Do not retry `permanent` errors, as they will never resolve without code changes.

### Where does ego-browser look for elements first—refs, roles, or CSS selectors?

The resolver follows a strict priority order defined in `resolveElementCenter` and `resolveElementObjectId`. It first checks for **refs** (strings starting with `@`), then attempts **role/name** lookups via the Accessibility tree (strings starting with `loc=role:`), and finally falls back to raw **CSS/XPath/text** locators evaluated through `Runtime.evaluate`. Understanding this order helps determine which CDP calls to inspect when debugging.