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 atelement-resolver.ts:63‑71, it handles refs, role/name lookup, or falls back to a raw selector.resolveElementObjectId– Returns a CDPobjectIdfor an element using the same resolution order. Seeelement-resolver.ts:149‑155.ElementResolutionError– The custom error type that carries akindproperty. Defined atelement-resolver.ts:4‑10.matchCountKind– Decides whether a “matched N elements” message is transient or permanent. Found atelement-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:
- Refs – If the selector starts with
@, the resolver callsparseRefand checksrefMap.get. - Role/Name – If the selector uses
loc=role:, the resolver queries the Accessibility tree viaAccessibility.getFullAXTree. - 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
waitForSelectorhelper 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
axNameMatchesfor 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
ElementResolutionErrorto access thekindproperty and failure message. - Interpret
kind:transientmeans retry (timing/DOM not ready),permanentmeans 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, orRuntime.evaluatecalls to identify the exact failure point. - Validate locators: Ensure CSS, XPath, and role selectors follow the syntax expected by
buildLocatorFindJsandaxNameMatches. - Use the test suite: Reference
element-resolver.test.mjsto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →