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

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, 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.
  • ElementResolutionError – The custom error type that carries a kind property. Defined at element-resolver.ts:4‑10.
  • matchCountKind – Decides whether a “matched N elements” message is transient or permanent. Found at element-resolver.ts:46‑50.

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:

// 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:

// 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:

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:

// 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

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 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 Stores the mapping from @N refs to backend node IDs, roles, and names.
src/locator-query.ts Generates the browser-side JavaScript snippets used by the resolver.
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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →