How the Element Resolver Distinguishes Between Transient and Permanent Resolution Failures in ego-lite
The element resolver uses a kind field on ElementResolutionError to classify failures as "transient" (retryable) or "permanent" (non-retryable), with the distinction determined by failure type, selector evaluation results, and ref-ID validity.
The element resolver in ego-lite converts CSS selectors or ref-IDs (like @21) into concrete element coordinates or object IDs. When resolution fails, the system must decide whether retrying could succeed. This article examines how ego-lite implements this classification based on the source code in citrolabs/ego-lite.
The kind Field: Core Mechanism
Every ElementResolutionError carries a kind property with two possible values:
"transient"— The failure may resolve on retry (e.g., stale ref, missing snapshot data)"permanent"— The failure will persist regardless of retries (e.g., invalid selector, ambiguous matches)
Callers inspect this field to implement appropriate retry logic or immediate error surfacing.
How Resolution Failures Are Classified
The resolver applies four distinct rules to determine kind, implemented in element-resolver.ts.
Multiple Selector Matches: Permanent Failure
When a selector matches more than one element, the resolver cannot determine user intent. The helper matchCountKind() parses error messages for patterns like "matched 3 elements" and returns "permanent" when the count exceeds 1.
// element-resolver.ts lines 46-50
if (matchCount > 1) {
return "permanent"; // Cannot disambiguate multiple matches
}
return "transient"; // Zero matches might resolve after DOM changes
This conservative approach treats ambiguity as unrecoverable.
Invalid Selector Syntax: Permanent Failure
Evaluation errors that don't match the "matched N elements" pattern indicate broken selector syntax or runtime exceptions. The selectorResolutionError() wrapper unconditionally assigns kind: "permanent".
// element-resolver.ts lines 52-60
function selectorResolutionError(originalError: Error): ElementResolutionError {
// Parse message for match count to determine if retryable
const kind = matchCountKind(originalError.message);
return new ElementResolutionError(
`Selector resolution failed: ${originalError.message}`,
kind === "transient" ? "transient" : "permanent"
);
}
Syntax errors cannot be fixed by waiting—the selector itself must change.
Missing or Stale Ref-ID: Transient Failure
Ref-IDs reference cached backend nodes that may become invalid after DOM mutations. The resolver throws "transient" because a fresh snapshot can repopulate the ref map.
// element-resolver.ts lines 74-75
throw new ElementResolutionError(`Unknown ref: ${refId}`, "transient");
This enables automatic recovery through snapshot refresh cycles.
Unavailable Box Model: Transient Failure
When a node exists but lacks render geometry (not yet displayed or hidden), the error propagates unchanged with "transient" status. Lines 94-99 in resolveElementCenter preserve this classification, allowing retries after navigation or visibility changes.
Practical Implementation Patterns
Basic Error Handling with Conditional Retry
try {
const { x, y } = await ego.resolveElementCenter(selectorOrRef);
// Use coordinates...
} catch (e) {
if (e instanceof ego.ElementResolutionError && e.kind === "transient") {
await ego.waitForIdle(0.5);
return await ego.resolveElementCenter(selectorOrRef); // Retry once
}
// Permanent: surface to user
console.error("Resolution failed permanently:", e.message);
throw e;
}
Robust Multi-Attempt Resolution for Stale Refs
async function safeResolveRef(ref, maxAttempts = 3) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await ego.resolveElementObjectId(ref);
} catch (e) {
if (e.kind === "transient") {
await ego.takeSnapshot(); // Force ref map refresh
continue;
}
throw e; // Permanent failure aborts immediately
}
}
throw new Error(`Failed to resolve ref after ${maxAttempts} attempts`);
}
Source File Reference
| File | Location | Purpose |
|---|---|---|
element-resolver.ts |
package/ego-browser/src/ |
Core resolution logic with kind assignment |
element-resolver.test.mjs |
package/ego-browser/src/ |
Test verification of transient/permanent behavior |
The implementation ensures that retry resources are spent only on failures where DOM state changes could yield success, while permanent configuration issues receive immediate, clear feedback.
Summary
ElementResolutionError.kindenables programmatic distinction between retryable and fatal resolution failures- Permanent failures: multiple selector matches, invalid syntax, evaluation exceptions
- Transient failures: stale ref-IDs, missing snapshot data, unavailable box models
- The
matchCountKind()helper parses error messages to disambiguate zero matches (transient) from multiple matches (permanent) - Callers should implement exponential backoff with snapshot refresh for transient errors and immediate user notification for permanent errors
Frequently Asked Questions
What happens if I retry a permanent resolution failure?
Retrying a permanent failure wastes resources and delays error reporting. The same invalid selector or ambiguous match will persist across all attempts. The ego-lite source code guarantees this through the hard-coded "permanent" assignment in selectorResolutionError().
How does the resolver handle zero selector matches versus multiple matches?
Zero matches receive "transient" classification because the DOM may eventually contain the target element. Multiple matches receive "permanent" because the resolver cannot determine which matching element the caller intended. The matchCountKind() function implements this split at lines 46-50.
Can transient failures become permanent?
No—a transient classification never converts to permanent within a single resolution attempt. However, repeated transient failures may exhaust caller retry limits, causing the operation to fail with a timeout or attempt-exceeded error rather than a resolution error.
Where should I implement retry logic for transient errors?
Implement retry loops in calling code rather than inside the resolver. The resolver remains stateless and deterministic, leaving retry policy (attempt count, delays, snapshot triggers) to application-specific handlers. The safeResolveRef() example above demonstrates this pattern.
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 →