How the Element Resolver Differentiates Transient vs Permanent Errors for Retry Logic in Ego-Lite
The Ego-Lite element resolver categorizes every lookup failure as either "transient" (retryable) or "permanent" (non-retryable) by inspecting where and why the failure occurred, then exposes this distinction through the kind property of ElementResolutionError.
The element‑resolver module sits at the heart of the Ego Browser's element-lookup pipeline. Every public helper — resolveElementCenter, resolveElementObjectId, resolveLocatorCenter, and others — delegates to this resolver. When lookups fail, the resolver throws an ElementResolutionError with a kind field that signals whether the caller should retry or abort immediately. This design provides a single source of truth for retry semantics across the entire API.
The ElementResolutionError Class
The error classification begins with a simple, strongly-typed structure. In package/ego-browser/src/element-resolver.ts, the ElementResolutionError class is defined as:
class ElementResolutionError extends Error {
kind: "transient" | "permanent";
constructor(message: string, kind: "transient" | "permanent") {
super(message);
this.name = "ElementResolutionError";
this.kind = kind;
}
}
All resolver entry points catch low-level CDP or runtime errors and re-throw them as ElementResolutionError with the appropriate kind. Higher-level helpers like waitForSelector interpret this field to decide whether to poll again or propagate the failure.
How the Resolver Chooses Error Kind
The resolver's decision logic inspects failure context — what went wrong and where in the pipeline it happened. Here are the specific rules implemented in element-resolver.ts:
Selector Syntax Errors
When JavaScript evaluation throws an exception containing a "matched N elements" message, the helper matchCountKind() (lines 46–50) parses the message:
- More than one element matched → permanent (ambiguous selector)
- Zero or one element matched → transient (element may appear later)
// From element-resolver.ts lines 46-50
function matchCountKind(message: string): "transient" | "permanent" {
const match = /matched (\d+) elements/.exec(message);
if (!match) return "transient";
return parseInt(match[1], 10) > 1 ? "permanent" : "transient";
}
Generic Selector Failures
The selectorResolutionError() function (lines 52–60) always creates a permanent error when the selector cannot be parsed or evaluates to an invalid expression. The selector itself is malformed — retrying will not help.
Unknown or Stale References
References using the @N syntax that point to non-existent backend nodes are treated as transient (lines 73–76). The DOM may change or a re-snapshot may restore validity.
Missing Box Model
When boxModelCenter encounters an element without computed box model data (not rendered or zero-sized), it throws a transient error (lines 55–62). Callers poll until the element becomes visible.
Role-Based Lookup Results
Two functions handle role-based queries differently:
findBackendNodeIdByRoleName(lines 18–22): Throws transient when no match is found — the element may appear.findUniqueBackendNodeIdByRoleName(lines 70–88): Throws permanent when more than one match is returned — the role qualifier is ambiguous.
Locator Resolution Results
The resolveLocatorObjectId function applies count-based logic:
- Zero elements matched (lines 36–43): Transient — element may appear later.
- More than one element when single required (lines 48–55): Permanent — locator is ambiguous.
AX Tree Mapping Failures
When findBackendNodeIdsByRoleName encounters an accessibility node lacking backendDOMNodeId (lines 58–63), it throws permanent. The underlying page structure cannot be resolved through the accessibility tree.
Implementing Retry Logic with Error Kinds
The kind property enables clean, consistent retry implementation. Here's a pattern used throughout the codebase:
while (true) {
try {
await resolveElementCenter(cdp, sessionId, refMap, ref);
break; // success
} catch (e) {
if (e instanceof ElementResolutionError && e.kind === "transient") {
await sleep(100); // poll again
continue;
}
throw e; // permanent → propagate immediately
}
}
Manual Resolver Usage with Explicit Retry
For custom automation scripts, you can implement your own retry loop:
import { resolveElementCenter, ElementResolutionError } from "ego-browser/src/element-resolver.js";
async function clickRef(ref, cdp, sessionId, refMap) {
const maxRetries = 50;
let attempts = 0;
while (attempts++ < maxRetries) {
try {
const { x, y, sessionId: sid } = await resolveElementCenter(
cdp,
sessionId,
refMap,
ref
);
await cdp.sendRaw("Input.dispatchMouseEvent", {
type: "mousePressed",
x,
y,
button: "left"
}, sid);
return; // success
} catch (e) {
if (e instanceof ElementResolutionError && e.kind === "transient") {
await new Promise(r => setTimeout(r, 150));
continue; // retry after short pause
}
throw e; // permanent → give up immediately
}
}
throw new Error(`Failed to click ${ref} after ${maxRetries} attempts`);
}
Using Higher-Level Helpers
Most users rely on helpers that embed this logic automatically:
import { click } from "ego-browser/src/helpers.js";
// Internally handles transient retries with exponential backoff
await click("@5");
The click, waitForSelector, and similar helpers in helpers.ts consume the resolver's errors and implement appropriate retry strategies based on kind.
Key Source Files
| File | Purpose | Critical Lines |
|---|---|---|
package/ego-browser/src/element-resolver.ts |
Error classification, ElementResolutionError class, all resolver entry points |
18–88 (core logic), 46–50 (matchCountKind), 52–60 (selectorResolutionError), 70–88 (role-based resolution) |
package/ego-browser/src/helpers.ts |
Public API (click, waitForSelector) consuming resolver errors |
Retry loop implementations |
package/ego-browser/src/element-resolver.test.mjs |
Test coverage for transient vs permanent scenarios | Edge case validation |
Summary
-
Transient errors (
kind: "transient") indicate conditions that may resolve with time: missing elements, stale references, unrendered nodes, and zero-count locator results. Callers should retry. -
Permanent errors (
kind: "permanent") indicate fundamental problems: ambiguous selectors, malformed expressions, multiple matches when one required, and unresolvable AX tree mappings. Callers should abort. -
The
ElementResolutionErrorclass inelement-resolver.tsprovides the single source of truth for this classification. -
All resolver functions (
resolveElementCenter,resolveElementObjectId,resolveLocatorObjectId, etc.) normalize failures into this error type with appropriatekindvalues. -
Higher-level APIs leverage this categorization for automatic retry behavior without exposing complexity to end users.
Frequently Asked Questions
What happens when a selector matches multiple elements?
The resolver throws a permanent ElementResolutionError. In resolveLocatorObjectId (lines 48–55), when a single element is required but multiple are found, the error kind is set to "permanent" because the selector is ambiguous. Retry would repeatedly match the same multiple elements.
Can I override the transient/permanent classification?
No. The kind property is determined entirely by the resolver's internal logic based on failure context. However, you can intercept any ElementResolutionError and implement custom fallback behavior before deciding whether to retry or abort.
How does the resolver handle stale element references?
References prefixed with @ that no longer map to valid backend node IDs produce transient errors (lines 73–76). The DOM may have changed through JavaScript execution, and a subsequent re-snapshot may restore reference validity. This enables robust automation against dynamic single-page applications.
What's the performance impact of transient error retries?
Transient retries with short sleeps (typically 50–150ms) introduce minimal overhead for element appearance scenarios. For permanent errors, the resolver fails fast without polling, avoiding wasted cycles on unrecoverable conditions. The test suite in element-resolver.test.mjs validates that permanent errors propagate immediately.
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 →