How to Distinguish Transient vs Permanent Element Resolution Errors in ego-lite
In ego-lite, every ElementResolutionError carries a kind property set to either "transient" (retryable) or "permanent" (non-retryable), determined by the specific failure condition encountered during DOM resolution.
The ego-lite browser automation harness employs a sophisticated error classification system to help automation scripts decide when to retry failed element lookups. Understanding how to distinguish transient vs permanent element resolution errors is critical for building robust automation that avoids infinite loops on unrecoverable selector issues while gracefully handling temporary DOM unavailability. This distinction is implemented in the core resolution logic within src/element-resolver.ts.
Understanding the ElementResolutionError Class
At the heart of ego-lite's error handling is the ElementResolutionError class defined in src/element-resolver.ts. Unlike generic exceptions, this error exposes a readonly kind property that is strictly typed as either "transient" or "permanent" at instantiation time. When writing automation scripts, checking error.kind provides the semantic information needed to implement appropriate retry policies.
Transient errors represent conditions that may resolve themselves after a page update, layout shift, or snapshot refresh. Permanent errors indicate fundamental problems with the selector or DOM structure that will persist across retries unless the underlying selector logic changes.
When Errors Are Classified as Transient
The resolver categorizes specific failure modes as transient because they typically arise from timing issues or temporary DOM states:
-
Box-model computation failures: When
boxModelCenter()cannot compute a center point because thecontentarray is empty or too short (lines 53-62), the error is transient because the element may appear after a render or layout change. -
Zero locator matches: The
matchCountKind()helper returns"transient"when a CSS, role, or text locator finds exactly 0 elements (lines 46-50). This signals that the element has not yet rendered or is temporarily detached from the DOM. -
Unknown reference IDs: When
resolveElementCenter()orresolveElementObjectId()encounters a numeric ref (e.g.,@N) missing from the ref map, it throws a transient error (lines 73-76), allowing time for the snapshot to refresh and the ref to become available. -
Backend node staleness: If
DOM.getBoxModelthrows but the fallback role/name lookup succeeds, the operation resolves. Only if the fallback also fails does it potentially throw a transient error, giving the agent another chance to resolve the element. -
Role locator zero matches: The
findBackendNodeIdByRoleName()function throws transient errors when no nodes match the role criteria (lines 418-421), as the accessibility tree may populate asynchronously.
When Errors Are Classified as Permanent
Permanent errors indicate selector ambiguity or structural impossibilities that retrying cannot resolve:
-
Multiple locator matches: When
matchCountKind()receives a count greater than 1, it returns"permanent"(referenced inselectorResolutionError()at lines 46-50 and explicitly thrown inresolveLocatorObjectId()/resolveLocatorCenter()at lines 249-254 and 350-354). An ambiguous selector requires modification, not repetition. -
Missing accessibility node IDs: While processing a role locator, if an AX node lacks the
backendDOMNodeIdproperty, the resolver throws a permanent error (lines 60-62). This indicates a malformed accessibility tree that will not self-correct. -
Role locator ambiguity: The
findUniqueBackendNodeIdByRoleName()function throws permanent errors when more than one node matches the role criteria (lines 82-87), as the selector is inherently ambiguous.
Implementing Retry Logic
The classification system enables intelligent automation patterns. Here is how to implement retry logic that distinguishes between recoverable and fatal errors:
import { resolveElementCenter, ElementResolutionError } from "ego-browser";
/** Retry transient errors up to 5 times with exponential backoff */
async function clickWhenReady(cdp, refMap, selector) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
const { x, y, sessionId } = await resolveElementCenter(
cdp,
undefined,
refMap,
selector,
);
// Perform CDP click using coordinates...
return;
} catch (e) {
if (e instanceof ElementResolutionError && e.kind === "transient") {
await new Promise(r => setTimeout(r, 200 * Math.pow(2, attempt)));
continue;
}
// Permanent or unknown error – fail fast
throw e;
}
}
throw new Error("Element resolution timeout after transient retries");
}
/** Handle permanent errors with immediate termination */
try {
await resolveElementCenter(cdp, undefined, refMap, "loc=css:.duplicate");
} catch (e) {
if (e instanceof ElementResolutionError && e.kind === "permanent") {
console.error("Selector ambiguity detected – fix required:", e.message);
process.exit(1);
}
}
Source Code Architecture
Several files collaborate to provide the error classification system:
-
src/element-resolver.ts: Contains theElementResolutionErrorclass, thematchCountKind()discriminator, and all resolution functions that instantiate these errors with appropriatekindvalues. -
src/element-resolver.test.mjs: Validates the transient vs permanent behavior, particularly around box-model errors (lines 47-66), ensuring the classification logic remains stable across refactors. -
src/ref-map.ts: Manages the mapping between numeric refs and backend node IDs; missing entries here trigger the transient errors handled by the resolver. -
src/helpers.ts: Higher-level automation helpers (click,waitForSelector) consume the error kinds to implement automatic retry semantics without exposing low-level resolution details to end users.
Summary
-
Transient errors (
kind === "transient") indicate temporary conditions such as missing box models, zero matches, or unknown refs that may resolve after a DOM update or snapshot refresh. -
Permanent errors (
kind === "permanent") indicate fatal conditions like selector ambiguity (multiple matches) or malformed accessibility nodes that require selector modification to resolve. -
The classification logic resides primarily in
src/element-resolver.ts, specifically within thematchCountKind()helper and various resolver functions. -
Automation scripts should retry on transient errors using exponential backoff and fail fast on permanent errors to avoid wasteful computation on unrecoverable selector failures.
Frequently Asked Questions
How does ego-lite determine if an element resolution error is transient or permanent?
The decision logic is centralized in src/element-resolver.ts. The matchCountKind() function maps match counts to error kinds (0 or 1 matches yield transient, >1 yields permanent), while specific failure modes like missing box models or unknown refs trigger hardcoded transient errors. AX node structural failures trigger permanent errors. Each ElementResolutionError receives its kind property at instantiation based on these rules.
What should I do when I encounter a permanent element resolution error?
When you catch an ElementResolutionError with kind === "permanent", immediately halt the current operation and report the failure. Permanent errors indicate selector ambiguity or structural DOM issues that will not resolve with time. You must modify the locator strategy (e.g., make CSS selectors more specific, add role constraints, or use unique identifiers) before retrying.
Can a transient error become permanent, or vice versa?
While a specific error instance is immutable once thrown, the underlying condition can change between attempts. A selector that returns zero matches (transient) might return one match after a page update, resolving the issue. Conversely, if a selector suddenly matches multiple elements due to dynamic DOM changes, a previously successful lookup could throw a permanent error. The error kind reflects the current state, not historical behavior.
Where is the error classification logic located in the ego-lite source code?
The primary classification logic resides in package/ego-browser/src/element-resolver.ts. Key functions include matchCountKind() (lines 46-50), boxModelCenter() (lines 53-62), and the resolution functions resolveLocatorObjectId(), resolveLocatorCenter(), and findUniqueBackendNodeIdByRoleName(). The test suite in src/element-resolver.test.mjs validates these behaviors against expected transient and permanent outcomes.
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 →