Handling Transient vs Permanent Element Resolution Errors in Ego-Lite
Ego-Lite throws ElementResolutionError instances with a kind property—set to either "transient" or "permanent"—to differentiate between temporary DOM conditions that may resolve on retry and fatal selector issues that require immediate failure.
The citrolabs/ego-lite browser automation framework implements a deterministic error classification system in src/element-resolver.ts to handle unreliable element lookups. When resolving CSS selectors, role locators, or @ref identifiers fails, the runtime categorizes the failure based on whether the condition is recoverable, enabling intelligent retry logic in higher-level APIs like waitForSelector and readElement.
The ElementResolutionError Class
At the core of this mechanism lies the ElementResolutionError class defined in src/element-resolver.ts. According to the citrolabs/ego-lite source code, the error constructor accepts a message string and a kind discriminator:
export class ElementResolutionError extends Error {
kind: "transient" | "permanent";
constructor(message: string, kind: "transient" | "permanent") {
super(message);
this.name = "ElementResolutionError";
this.kind = kind;
}
}
This structure allows calling code to inspect error.kind and decide whether to retry the operation or abort immediately.
Classification Logic for Error Kinds
The resolver determines the error kind based on the specific failure mode encountered during element lookup. As implemented in src/element-resolver.ts, the following rules apply:
Multiple element matches. When a selector matches more than one element but a single unique element is required, the error is permanent. The matchCountKind() helper (lines 46-50) classifies ambiguous matches as permanent because the caller must disambiguate the selector.
Invalid selector syntax. Syntax errors in CSS selectors or role locators result in permanent errors. The selectorResolutionError() function (lines 52-60) marks these as permanent since an invalid selector cannot become valid on subsequent retries.
Unknown @ref identifiers. References to snapshot IDs that do not exist in the current DOM generate transient errors (lines 74-75). The element may appear after the next snapshot, making retry appropriate.
Missing box model or stale backend nodes. When the Chrome DevTools Protocol reports a missing box model or stale node, element-resolver.ts (lines 58-62) treats these as transient conditions indicating the element is not yet rendered or ready.
Accessibility nodes without backend DOM node IDs. If an AX node lacks a backendDOMNodeId, the error is permanent (lines 59-63) because the accessibility node cannot be linked to a concrete DOM element.
Zero locator matches. When a locator finds no elements, the error is transient because the element may appear later after navigation or DOM updates.
How Callers Handle Each Error Type
Higher-level APIs in src/driver/waits.ts and src/driver/locator.ts implement distinct strategies based on the error classification.
waitForSelector Retry Logic
The waitForSelector function in src/driver/waits.ts (lines 505-509) catches ElementResolutionError and implements polling logic:
try {
handle = await resolveHandle(selector);
} catch (err) {
if (err instanceof ElementResolutionError && err.kind === "transient") {
await state.sleep(300);
continue; // keep polling
}
throw err; // permanent → fail fast
}
Transient errors trigger a 300ms sleep and retry loop until timeout, while permanent errors propagate immediately to prevent wasted cycles on impossible selectors.
readElement Polling Behavior
The readElement utility in src/driver/locator.ts (lines 11-18) implements deadline-based polling for transient failures:
if (!(error instanceof ElementResolutionError) ||
error.kind !== "transient" ||
state.now() >= deadline) {
throw error; // permanent or timeout
}
await state.sleep(Math.min(100, deadline - state.now()));
This pattern retries transient resolution errors with short sleeps until the default timeout expires, ensuring resilience against temporary DOM churn.
readOptionalElement Fallback Strategy
For optional elements, readOptionalElement in src/driver/locator.ts (lines 36-40) treats transient errors as "element not present" rather than exceptions:
} catch (error) {
if (error instanceof ElementResolutionError && error.kind === "transient") {
return fallback;
}
throw error;
}
Permanent errors still propagate to signal configuration bugs, while transient errors gracefully return the fallback value.
Practical Implementation Examples
Example 1: Using waitForSelector for Robust Waiting
The high-level waitForSelector helper automatically handles the transient/permanent distinction:
// Wait up to the default timeout for a button to become visible.
await waitForSelector('button[data-testid="submit"]', { state: "visible" });
If the button is not yet rendered, the underlying ElementResolutionError is transient, triggering retries. If the selector is malformed, a permanent error rejects the promise immediately.
Example 2: Manual Retry with resolveElementCenter
For custom interactions, manually check error.kind before retrying:
import { resolveElementCenter, ElementResolutionError } from "./element-resolver.js";
async function clickCenter(cdp, sessionId, refMap, selectorOrRef) {
try {
const { x, y, sessionId: sid } = await resolveElementCenter(
cdp,
sessionId,
refMap,
selectorOrRef,
);
await cdp("Input.dispatchMouseEvent", { type: "mousePressed", x, y }, sid);
} catch (e) {
if (e instanceof ElementResolutionError && e.kind === "transient") {
// Retry after a short pause – element may appear later.
await new Promise(r => setTimeout(r, 200));
return clickCenter(cdp, sessionId, refMap, selectorOrRef);
}
// Permanent error: surface to the user.
throw e;
}
}
Example 3: Optional Element Fetching
Use readOptionalElement to safely attempt retrieval with a fallback:
import { readOptionalElement } from "./driver/locator.js";
const maybeButton = await readOptionalElement(
'button[data-testid="optional"]',
'function(el){ return el?.innerText; }',
[],
null, // fallback when not present
);
if (maybeButton !== null) {
console.log("Button text:", maybeButton);
}
If the selector yields a transient error, the function returns null without throwing.
Summary
- Transient errors indicate temporary DOM conditions—such as elements not yet rendered, stale backend nodes, unknown
@refIDs, or zero matches—that may resolve on retry. The runtime implements automatic polling and waiting strategies when encountering these. - Permanent errors signal fundamental issues—ambiguous selectors matching multiple elements, invalid syntax, or orphaned accessibility nodes—that cannot resolve through waiting. These propagate immediately to alert developers of configuration errors.
- The classification occurs in
src/element-resolver.tsthrough theElementResolutionErrorclass and helper functions likematchCountKind()andselectorResolutionError(). - Higher-level APIs in
src/driver/waits.tsandsrc/driver/locator.tsconsume this classification to implement fail-fast behavior for permanent issues and resilient polling for transient ones.
Frequently Asked Questions
How does Ego-Lite decide if an element resolution error is transient or permanent?
Ego-Lite examines the specific failure mode in src/element-resolver.ts. Invalid selector syntax and ambiguous matches (multiple elements) produce permanent errors because the code requires correction. Missing elements, stale backend nodes, and unknown @ref IDs generate transient errors because the DOM may update to satisfy the lookup on subsequent attempts.
What happens when waitForSelector encounters a permanent resolution error?
When waitForSelector in src/driver/waits.ts catches an ElementResolutionError with kind === "permanent", it re-throws the error immediately without additional retries. This fail-fast behavior prevents infinite polling loops on malformed selectors or ambiguous locators.
Can I handle transient resolution errors manually in my Ego-Lite scripts?
Yes. Import ElementResolutionError from src/element-resolver.ts and check the kind property in your catch blocks. If the error is transient, implement a retry loop with exponential backoff or return a fallback value, while allowing permanent errors to propagate to the caller.
Why does readOptionalElement return a fallback for transient errors but throw for permanent ones?
The design treats transient errors as indicating "element not yet available," which aligns with the semantic meaning of an optional element being absent. Permanent errors indicate a broken selector or DOM structure that will never resolve, so the function throws to alert you of the configuration bug rather than silently returning a fallback.
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 →