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

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

  • 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) 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.

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:

    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:

    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:

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

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

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:

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 (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 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.

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 →