How ego-lite's element-resolver Classifies Failures as Retryable vs Permanent
ego-lite wraps every element resolution failure in an ElementResolutionError with a kind field set to "transient" for retryable errors or "permanent" for unrecoverable ones.
The element-resolver.ts file in ego-lite is the single source of truth for this classification. When a CDP-based element lookup fails, the resolver does not simply throw a generic error. Instead, it inspects the failure context and explicitly chooses whether the problem might resolve itself (transient) or requires code changes (permanent).
The ElementResolutionError Class
At the heart of this system is the error class defined in package/ego-browser/src/element-resolver.ts:
export class ElementResolutionError extends Error {
kind: "transient" | "permanent";
constructor(message: string, kind: "transient" | "permanent") {
super(message);
this.name = "ElementResolutionError";
this.kind = kind;
}
}
Every failure path in the resolver instantiates this class with the appropriate kind. Callers in driver/waits.ts and driver/locator.ts then branch on err.kind to decide whether to retry or abort.
How element-resolver Decides: Transient vs Permanent
The classification logic follows consistent patterns based on the nature of the failure.
Selector Match Count Failures
The matchCountKind() function (lines 46-49) parses error messages about match counts:
- More than one match →
permanent(ambiguous selector cannot self-resolve) - Zero matches →
transient(element may appear in a future DOM snapshot)
This applies to both CSS/XPath selectors and generic locators. For example, locatorCount() at line 38 returns zero → transient; line 49 finds more than one → permanent.
Invalid Selector Syntax
When CDP evaluation throws an exception, selectorResolutionError() (line 52) always returns permanent. A syntax error in the selector string requires a code change to fix.
Reference Resolution Failures
In resolveElementCenter() and resolveElementObjectId(), missing or stale references (line 74) are transient. The backend node ID may exist in a subsequent snapshot, so retrying is worthwhile.
Accessibility Tree Failures
Two distinct cases in findBackendNodeIdsByRoleName() and findUniqueBackendNodeIdByRoleName():
| Condition | Classification | Reason |
|---|---|---|
backendDOMNodeId missing from AX node (line 58) |
permanent |
Configuration problem; no DOM node to address |
| Zero role matches (line 76) | transient |
Element not yet rendered |
| Multiple role matches (line 82) | permanent |
Ambiguous role/name criteria |
Practical Error Handling Examples
Handling Retryable Resolution Errors
import { resolveElementCenter, ElementResolutionError } from "./element-resolver.js";
try {
const { x, y } = await resolveElementCenter(
cdp,
sessionId,
refMap,
"loc=css:button.submit"
);
} catch (e) {
if (e instanceof ElementResolutionError) {
if (e.kind === "transient") {
// Retry after delay — DOM may update
await delay(100);
return retry();
} else {
// Permanent — selector needs fixing
throw new Error(`Invalid selector: ${e.message}`);
}
}
}
Detecting Ambiguous Role-Based Locators
import { resolveElementObjectId, ElementResolutionError } from "./element-resolver.js";
try {
const obj = await resolveElementObjectId(
cdp,
sessionId,
refMap,
"role=button name=Submit"
);
} catch (e) {
if (e instanceof ElementResolutionError && e.kind === "permanent") {
// Refine role criteria or add name disambiguation
console.error("Ambiguous locator — multiple buttons match 'Submit'");
}
}
How Callers Consume the Classification
The kind field drives retry logic throughout ego-lite's driver layer. In package/ego-browser/src/driver/waits.ts, wait loops inspect err.kind to determine whether to continue polling. The locator.ts wrapper propagates the same classification to higher-level APIs. This creates a clean separation: the resolver decides what went wrong; the caller decides what to do about it.
Summary
- Transient (
"transient"): Zero matches, missing refs, stale snapshots — conditions that may resolve with time or DOM updates - Permanent (
"permanent"): Multiple matches, invalid syntax, missingbackendDOMNodeId— conditions requiring code or configuration changes - Source of truth:
package/ego-browser/src/element-resolver.tsmakes allkinddecisions at error creation time - Consumption pattern: Callers in
waits.tsandlocator.tsbranch onerr.kindfor retry vs abort logic
Frequently Asked Questions
What makes an element resolution error retryable in ego-lite?
A failure is transient when the underlying condition might self-resolve: zero matches (element not yet in DOM), missing or stale reference IDs, or temporary snapshot inconsistencies. The resolver assumes the DOM may update and the same operation could succeed later.
Why are multiple matches classified as permanent failures?
Multiple matches indicate an ambiguous selector that will not become unambiguous without code changes. Whether caused by overly broad CSS selectors or non-unique role/name combinations, the resolver treats this as a permanent ElementResolutionError because retrying cannot reduce the match count.
Where should I handle ElementResolutionError in my code?
Handle it at the point of element interaction or in wrapper utilities. The driver/waits.ts and driver/locator.ts files in ego-lite demonstrate the recommended pattern: catch ElementResolutionError, inspect err.kind, retry if transient, and propagate or abort if permanent.
Can I override the retryable vs permanent classification?
No — the classification is hardcoded in element-resolver.ts based on failure type. If you need different retry behavior, wrap the resolver call and implement your own retry policy based on err.kind, or modify the selector to avoid permanent error conditions like ambiguity or syntax errors.
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 →